summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBob Ippolito <bob@redivi.com>2021-11-28 08:40:27 -0800
committerBob Ippolito <bob@redivi.com>2021-11-28 08:45:25 -0800
commit96294f571af66bfe820cc1e64e776a14217af273 (patch)
tree38c72c4ef20f15ac55ec112bb301d2fb7746a14b
parenta9c25a2ae1101bf50f470e51649eb68b56c30f36 (diff)
parente36a3c87580477e8dbc407bd5ef7c1ed1e987cf0 (diff)
downloadxattr-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.yml116
-rw-r--r--.travis.yml59
-rwxr-xr-x.travis/install.sh33
-rwxr-xr-x.travis/run.sh29
-rw-r--r--CHANGES.txt9
-rw-r--r--README.rst3
-rw-r--r--pyproject.toml14
-rw-r--r--setup.py7
-rw-r--r--xattr/__init__.py2
-rw-r--r--xattr/tests/__init__.py1
-rw-r--r--xattr/tests/test_tool.py117
-rwxr-xr-xxattr/tool.py69
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
diff --git a/README.rst b/README.rst
index c2c9214..a1de1fb 100644
--- a/README.rst
+++ b/README.rst
@@ -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
diff --git a/setup.py b/setup.py
index 0f1af60..928ab2a 100644
--- a/setup.py
+++ b/setup.py
@@ -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))