diff options
author | Bob Ippolito <bob@redivi.com> | 2021-11-28 08:40:27 -0800 |
---|---|---|
committer | Bob Ippolito <bob@redivi.com> | 2021-11-28 08:45:25 -0800 |
commit | 96294f571af66bfe820cc1e64e776a14217af273 (patch) | |
tree | 38c72c4ef20f15ac55ec112bb301d2fb7746a14b | |
parent | a9c25a2ae1101bf50f470e51649eb68b56c30f36 (diff) | |
parent | e36a3c87580477e8dbc407bd5ef7c1ed1e987cf0 (diff) | |
download | xattr-add-clear-option.tar.gz |
Merge remote-tracking branch 'origin/master' into add-clear-optionadd-clear-option
-rw-r--r-- | .github/workflows/build-and-deploy.yml | 116 | ||||
-rw-r--r-- | .travis.yml | 59 | ||||
-rwxr-xr-x | .travis/install.sh | 33 | ||||
-rwxr-xr-x | .travis/run.sh | 29 | ||||
-rw-r--r-- | CHANGES.txt | 9 | ||||
-rw-r--r-- | README.rst | 3 | ||||
-rw-r--r-- | pyproject.toml | 14 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | xattr/__init__.py | 2 | ||||
-rw-r--r-- | xattr/tests/__init__.py | 1 | ||||
-rw-r--r-- | xattr/tests/test_tool.py | 117 | ||||
-rwxr-xr-x | xattr/tool.py | 69 |
12 files changed, 309 insertions, 150 deletions
diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..ad5a636 --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,116 @@ +name: Build and upload to PyPI + +# Build on every branch push, tag push, and pull request change: +on: [push, pull_request] +# Alternatively, to publish when a (published) GitHub Release is created, use the following: +# on: +# push: +# pull_request: +# release: +# types: +# - published + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - 'ubuntu-latest' + - 'macos-latest' + + steps: + - uses: actions/checkout@v2 + + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v1 + with: + platforms: arm64 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.2.2 + env: + CIBW_BUILD_FRONTEND: "build" + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_MANYLINUX_I686_IMAGE: manylinux2014 + CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014 + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: manylinux2014 + CIBW_MANYLINUX_PYPY_X86_64_IMAGE: manylinux2014 + CIBW_MANYLINUX_PYPY_I686_IMAGE: manylinux2014 + CIBW_ARCHS_LINUX: "auto aarch64" + CIBW_ARCHS_MACOS: "x86_64 universal2 arm64" + + - name: Build Python 2.7 wheels + if: runner.os != 'Windows' + uses: pypa/cibuildwheel@v1.12.0 + env: + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2010 + CIBW_MANYLINUX_I686_IMAGE: manylinux2010 + CIBW_BUILD: "cp27-*" + CIBW_SKIP: "pp*" + CIBW_ARCHS_LINUX: "auto aarch64" + + - uses: actions/upload-artifact@v2 + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/')" + with: + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.9' + + - name: Build sdist + run: python setup.py sdist + + - uses: actions/upload-artifact@v2 + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/')" + with: + path: dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + # upload to PyPI on every tag starting with 'v' + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')" + # alternatively, to publish when a GitHub Release is created, use the following rule: + # if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v2 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD }} + # To test: repository_url: https://test.pypi.org/legacy/ + + upload_pypi_test: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + # upload to PyPI on every tag starting with 'v' + if: "github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/test-v')" + # alternatively, to publish when a GitHub Release is created, use the following rule: + # if: github.event_name == 'release' && github.event.action == 'published' + steps: + - uses: actions/download-artifact@v2 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD_TEST }} + repository_url: https://test.pypi.org/legacy/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7e436af..0000000 --- a/.travis.yml +++ /dev/null @@ -1,59 +0,0 @@ -language: python -cache: - directories: - - "$HOME/.cache/pip" - - "$HOME/.pyenv" -matrix: - include: - - name: 'Ubuntu: 3.8' - os: linux - python: 3.8 - - name: 'Ubuntu: 3.7' - os: linux - python: 3.7 - - name: 'Ubuntu: 3.6' - os: linux - python: 3.6 - - name: 'Ubuntu: 3.5' - os: linux - python: 3.5 - - name: 'Ubuntu: 2.7' - os: linux - python: 2.7 - - name: 'Ubuntu: PyPy 3.5' - os: linux - env: PYPY_URL=https://bitbucket.org/pypy/pypy/downloads/pypy3-v6.0.0-linux64.tar.bz2 - - name: 'Ubuntu: PyPy 2.7' - os: linux - env: PYPY_URL=https://bitbucket.org/pypy/pypy/downloads/pypy2-v6.0.0-linux64.tar.bz2 - - name: 'OSX: 3.7' - os: osx - language: generic - env: PYENV_VERSION=3.7.4 BUILD_SDIST=true - osx_image: xcode_9.4 - - name: 'OSX: 3.6' - os: osx - language: generic - env: PYENV_VERSION=3.6.5 - osx_image: xcode_9.4 - - name: 'OSX: 2.7' - os: osx - language: generic - env: PYENV_VERSION=2.7.15 - osx_image: xcode_9.4 -install: -- "./.travis/install.sh" -script: -- "./.travis/run.sh" -deploy: - provider: releases - api_key: - secure: ik/Btxv+NMOGjKuNnilYSeATYwL7sHy8nildzQcF+GMCFL8mDcerXRoC1jOF+ETsmSOAZ95NOEUGNiyvCApy4VgYvBvz7mJzdaob034+GXOStEIdBBvV8v9XB9XwQpJUUGvRMSF9WMUGmhyQ9PQEPOHfERgLkdlcY24djCJm/6A= - file: - - dist/*.whl - - dist/*.tar.gz - file_glob: true - on: - repo: xattr/xattr - tags: true - skip_cleanup: true diff --git a/.travis/install.sh b/.travis/install.sh deleted file mode 100755 index 1ff4a03..0000000 --- a/.travis/install.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -set -e -set -x - -install_pypy() { - pypy_dir="/home/travis/pypy" - curl -L "$PYPY_URL" -o "pypy.tar.bz2" - mkdir "$pypy_dir" - tar xf "pypy.tar.bz2" -C "$pypy_dir" --strip-components=1 - if [ -f "$pypy_dir/bin/pypy" ]; then - ln -s "$pypy_dir/bin/pypy" "pypy" - elif [ -f "$pypy_dir/bin/pypy3" ]; then - ln -s "$pypy_dir/bin/pypy3" "pypy" - fi - ./pypy -m ensurepip -} - -install_pyenv() { - brew update > /dev/null - brew upgrade readline openssl pyenv - eval "$(pyenv init -)" - pyenv install -sv "$PYENV_VERSION" - pip install --upgrade pip - pyenv rehash - python -m pip install wheel -} - -if [[ -n "$PYPY_URL" ]]; then - install_pypy -elif [[ -n "$PYENV_VERSION" && "$TRAVIS_OS_NAME" == "osx" ]]; then - install_pyenv -fi diff --git a/.travis/run.sh b/.travis/run.sh deleted file mode 100755 index 206bf68..0000000 --- a/.travis/run.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -set -e -set -x - -if [[ -n "$PYENV_VERSION" ]]; then - eval "$(pyenv init -)" -fi - -if [[ -n "$PYPY_URL" ]]; then - cmd=./pypy -else - cmd=python -fi - -"$cmd" setup.py build_ext -i -"$cmd" -m compileall -f . -"$cmd" setup.py test - -if [[ -n "$PYENV_VERSION" && "$TRAVIS_OS_NAME" == 'osx' ]]; then - python setup.py bdist_wheel -fi - -if [[ "$BUILD_SDIST" == 'true' ]]; then - "$cmd" setup.py sdist --formats=gztar - # Ensure the package installs from tarball correctly. - filename=$("$cmd" setup.py --fullname) - pip install "dist/$filename.tar.gz" -fi diff --git a/CHANGES.txt b/CHANGES.txt index 5192a73..0db3bd2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,12 @@ +Version 0.9.8 released 2021-11-19 + +* Update build to use Github Actions + https://github.com/xattr/xattr/pull/95 +* Various dump related fixes + https://github.com/xattr/xattr/pull/93 +* Fix classifiers list + https://github.com/xattr/xattr/pull/89 + Version 0.9.7 released 2019-12-02 * Fix xattr().update() in Python 3 @@ -12,3 +12,6 @@ file system objects (files, directories, symlinks, etc). Extended attributes are currently only available on Darwin 8.0+ (Mac OS X 10.4) and Linux 2.6+. Experimental support is included for Solaris and FreeBSD. + +Note: On Linux, custom xattr keys need to be prefixed with the `user` +namespace, ie: `user.your_attr`. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..13cbddf --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.cibuildwheel] +test-requires = "pytest" +test-command = "pytest {project}/xattr/tests" + +[tool.cibuildwheel.linux] +before-all = "yum install -y libffi-devel" + +[[tool.cibuildwheel.overrides]] +select = "*-manylinux2_*" +before-all = "apt-get -y install libffi-dev" + +[[tool.cibuildwheel.overrides]] +select = "*-musllinux*" +before-all = "apk add libffi-dev"
\ No newline at end of file @@ -5,7 +5,7 @@ import sys from setuptools import setup -VERSION = '0.9.7' +VERSION = '0.9.8' DESCRIPTION = "Python wrapper for extended filesystem attributes" LONG_DESCRIPTION = """ Extended attributes extend the basic attributes of files and directories @@ -49,8 +49,9 @@ setup( "xattr = xattr.tool:main", ], }, - install_requires=["cffi>=1.0.0"], - setup_requires=["cffi>=1.0.0"], + # Keep this in sync with pyproject.toml + install_requires=["cffi>=1.0"], + setup_requires=["cffi>=1.0"], cffi_modules=["xattr/lib_build.py:ffi"], test_suite="xattr.tests.all_tests_suite", zip_safe=False, diff --git a/xattr/__init__.py b/xattr/__init__.py index 50780b4..4e5a517 100644 --- a/xattr/__init__.py +++ b/xattr/__init__.py @@ -7,7 +7,7 @@ The xattr type wraps a path or file descriptor with a dict-like interface that exposes these extended attributes. """ -__version__ = '0.9.7' +__version__ = '0.9.8' from .compat import integer_types from .lib import (XATTR_NOFOLLOW, XATTR_CREATE, XATTR_REPLACE, diff --git a/xattr/tests/__init__.py b/xattr/tests/__init__.py index 739dd58..3e75a10 100644 --- a/xattr/tests/__init__.py +++ b/xattr/tests/__init__.py @@ -6,6 +6,7 @@ import unittest def all_tests_suite(): suite = unittest.TestLoader().loadTestsFromNames([ 'xattr.tests.test_xattr', + 'xattr.tests.test_tool', ]) return suite diff --git a/xattr/tests/test_tool.py b/xattr/tests/test_tool.py new file mode 100644 index 0000000..644364f --- /dev/null +++ b/xattr/tests/test_tool.py @@ -0,0 +1,117 @@ +import contextlib +import errno +import io +import os +import shutil +import sys +import tempfile +import unittest +import uuid + +import xattr +import xattr.tool + + +class TestTool(unittest.TestCase): + def setUp(self): + self.test_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.test_dir) + + orig_stdout = sys.stdout + + def unpatch_stdout(sys=sys, orig_stdout=orig_stdout): + sys.stdout = orig_stdout + + self.addCleanup(unpatch_stdout) + sys.stdout = self.mock_stdout = io.StringIO() + + def getoutput(self): + value = self.mock_stdout.getvalue() + self.mock_stdout.seek(0) + self.mock_stdout.truncate(0) + return value + + @contextlib.contextmanager + def temp_file(self): + test_file = os.path.join(self.test_dir, str(uuid.uuid4())) + fd = os.open(test_file, os.O_CREAT | os.O_WRONLY) + try: + yield test_file, fd + finally: + os.close(fd) + + def set_xattr(self, fd, name, value): + try: + xattr.setxattr(fd, name, value) + except OSError as e: + if e.errno == errno.ENOTSUP: + raise unittest.SkipTest('xattrs are not supported') + raise + + def test_utf8(self): + with self.temp_file() as (file_path, fd): + self.set_xattr(fd, 'user.test-utf8', + u'\N{SNOWMAN}'.encode('utf8')) + self.set_xattr(fd, 'user.test-utf8-and-nul', + u'\N{SNOWMAN}\0'.encode('utf8')) + + xattr.tool.main(['prog', '-p', 'user.test-utf8', file_path]) + self.assertEqual(u'\N{SNOWMAN}\n', self.getoutput()) + + xattr.tool.main(['prog', '-p', 'user.test-utf8-and-nul', file_path]) + self.assertEqual(u''' +0000 E2 98 83 00 .... + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-l', file_path]) + output = self.getoutput() + self.assertIn(u'user.test-utf8: \N{SNOWMAN}\n', output) + self.assertIn(u''' +user.test-utf8-and-nul: +0000 E2 98 83 00 .... + +'''.lstrip(), output) + + def test_non_utf8(self): + with self.temp_file() as (file_path, fd): + self.set_xattr(fd, 'user.test-not-utf8', b'cannot\xffdecode') + + xattr.tool.main(['prog', '-p', 'user.test-not-utf8', file_path]) + self.assertEqual(u''' +0000 63 61 6E 6E 6F 74 FF 64 65 63 6F 64 65 cannot.decode + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-l', file_path]) + self.assertIn(u''' +user.test-not-utf8: +0000 63 61 6E 6E 6F 74 FF 64 65 63 6F 64 65 cannot.decode + +'''.lstrip(), self.getoutput()) + + def test_nul(self): + with self.temp_file() as (file_path, fd): + self.set_xattr(fd, 'user.test', b'foo\0bar') + self.set_xattr(fd, 'user.test-long', + b'some rather long value with\0nul\0chars in it') + + xattr.tool.main(['prog', '-p', 'user.test', file_path]) + self.assertEqual(u''' +0000 66 6F 6F 00 62 61 72 foo.bar + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-p', 'user.test-long', file_path]) + self.assertEqual(u''' +0000 73 6F 6D 65 20 72 61 74 68 65 72 20 6C 6F 6E 67 some rather long +0010 20 76 61 6C 75 65 20 77 69 74 68 00 6E 75 6C 00 value with.nul. +0020 63 68 61 72 73 20 69 6E 20 69 74 chars in it + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-l', file_path]) + self.assertIn(u''' +0000 66 6F 6F 00 62 61 72 foo.bar + +'''.lstrip(), self.getoutput()) diff --git a/xattr/tool.py b/xattr/tool.py index b936ff0..d9548e1 100755 --- a/xattr/tool.py +++ b/xattr/tool.py @@ -64,12 +64,19 @@ def usage(e=None): print(" -z: compress or decompress (if compressed) attribute value in zip format") if e: - sys.exit(64) + return 64 else: - sys.exit(0) + return 0 -_FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) +if sys.version_info < (3,): + ascii = repr + uchr = unichr +else: + uchr = chr + + +_FILTER = u''.join([(len(ascii(chr(x))) == 3) and uchr(x) or u'.' for x in range(256)]) def _dump(src, length=16): @@ -82,11 +89,11 @@ def _dump(src, length=16): return ''.join(result) -def main(): +def main(argv): try: - (optargs, args) = getopt.getopt(sys.argv[1:], "hlpwdzsc", ["help"]) + (optargs, args) = getopt.getopt(argv[1:], "hlpwdzsc", ["help"]) except getopt.GetoptError as e: - usage(e) + return usage(e) attr_name = None long_format = False @@ -97,11 +104,11 @@ def main(): nofollow = False compress = lambda x: x decompress = compress - status = 0 + errors = [] for opt, arg in optargs: if opt in ("-h", "--help"): - usage() + return usage() elif opt == "-l": long_format = True elif opt == "-s": @@ -109,39 +116,39 @@ def main(): elif opt == "-p": read = True if write or delete or clear: - usage("-p not allowed with -w, -d or -c") + return usage("-p not allowed with -w, -d or -c") elif opt == "-w": write = True if read or delete or clear: - usage("-w not allowed with -p, -d or -c") + return usage("-w not allowed with -p, -d or -c") elif opt == "-d": delete = True if read or write or clear: - usage("-d not allowed with -p, -w or -c") + return usage("-d not allowed with -p, -w or -c") elif opt == "-c": clear = True if read or write or delete: - usage("-c not allowed with -p, -w or -d") + return usage("-c not allowed with -p, -w or -d") elif opt == "-z": compress = zlib.compress decompress = zlib.decompress if write or delete or clear: if long_format: - usage("-l not allowed with -w, -d or -c") + return usage("-l not allowed with -w, -d or -c") if read or write or delete: if not args: - usage("No attr_name") + return usage("No attr_name") attr_name = args.pop(0) if write: if not args: - usage("No attr_value") + return usage("No attr_value") attr_value = args.pop(0).encode('utf-8') if len(args) == 0: - usage("No file") + return usage("No file") if len(args) > 1: multiple_files = True @@ -154,6 +161,7 @@ def main(): for filename in args: def onError(e): + errors.append(e) if not os.path.exists(filename): sys.stderr.write("No such file: %s\n" % (filename,)) else: @@ -205,31 +213,42 @@ def main(): file_prefix = "" for attr_name in attr_names: + should_dump = False try: try: attr_value = decompress(attrs[attr_name]) except zlib.error: attr_value = attrs[attr_name] - attr_value = attr_value.decode('utf-8') + try: + if b'\0' in attr_value: + # force dumping + raise NullsInString + attr_value = attr_value.decode('utf-8') + except (UnicodeDecodeError, NullsInString): + attr_value = attr_value.decode('latin-1') + should_dump = True except KeyError: onError("%sNo such xattr: %s" % (file_prefix, attr_name)) continue if long_format: - try: - if '\0' in attr_value: - raise NullsInString - print("".join((file_prefix, "%s: " % (attr_name,), attr_value))) - except (UnicodeDecodeError, NullsInString): + if should_dump: print("".join((file_prefix, "%s:" % (attr_name,)))) print(_dump(attr_value)) + else: + print("".join((file_prefix, "%s: " % (attr_name,), attr_value))) else: if read: - print("".join((file_prefix, attr_value))) + if should_dump: + if file_prefix: + print(file_prefix) + print(_dump(attr_value)) + else: + print("".join((file_prefix, attr_value))) else: print("".join((file_prefix, attr_name))) - sys.exit(status) + return 1 if errors else 0 if __name__ == "__main__": - main() + sys.exit(main(sys.argv)) |