diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8b42f08 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,44 @@ +name: Test + +on: [push, pull_request] + +env: + FORCE_COLOR: 1 + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + os: [ubuntu-24.04] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y libattr1-dev libfuse-dev + sudo apt-get install -y pkg-config gcc + + - name: Install Python dependencies (misc) + run: pip install setuptools pytest sphinx + + - name: Install Python dependencies + run: pip install "Cython>=3" + + - name: Test + run: | + set -e + python setup.py build_cython + python setup.py build_ext --inplace + pytest -v -rs test/ + + sphinx-build -b html rst doc/html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ade135 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +MANIFEST +build +dist +doc/html +doc/doctrees +*.egg-info +*.pyc +src/llfuse.c +src/llfuse*.so +.idea +.pytest_cache diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..d725739 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,28 @@ +# .readthedocs.yaml - Read the Docs configuration file. +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details. + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + jobs: + post_checkout: + - git fetch --unshallow + pre_install: + - pip install -r requirements.d/rtd.txt + - python setup.py build_cython + - python setup.py build_ext --inplace + apt_packages: + - build-essential + - pkg-config + - libfuse-dev + +python: + install: + - method: pip + path: . + +sphinx: + configuration: rst/conf.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1c4f5ab..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -sudo: required -language: python - -matrix: - include: - - python: "3.4" - os: linux - dist: trusty - - python: "3.5" - os: linux - dist: xenial - - python: "3.6" - os: linux - dist: bionic - - python: "3.7" - os: linux - dist: bionic - - python: "3.8" - os: linux - dist: focal - - python: "3.9-dev" - os: linux - dist: focal - -addons: - apt: - packages: - - libattr1-dev - - pkg-config - - gcc - - libfuse-dev -install: test/travis-install.sh -script: test/travis-test.sh diff --git a/Changes.rst b/Changes.rst index 0a71927..39bfb8d 100644 --- a/Changes.rst +++ b/Changes.rst @@ -4,9 +4,98 @@ .. currentmodule:: llfuse -**WARNING**: Python-LLFUSE is no longer actively maintained. Unless you are stuck -with Python 2.x or libfuse 2.x, we recommended to use the pyfuse3_ -module instead. +**WARNING**: Python-LLFUSE is no longer actively developed. + +Release 1.6.0 (not yet released) +================================ + +- Drop Python 3.8 and 3.9 support. +- Drop testing on Cython 0.29.x + + +Release 1.5.2 (2025-12-22) +========================== + +- Support and test on Python 3.14 also. +- CI: test on Ubuntu 24.04 +- Cythonized using Cython 3.2.3. +- setup.py: + + - use SPDX license metadata (the old style was deprecated), + also require setuptools >= 78.1.1, #104 + - remove tests_require (not supported anymore) +- get rid of sphinx build warnings, #56 +- README: link to mfusepy project + + +Release 1.5.1 (2024-08-31) +========================== + +- Support and test on Python 3.13 also. +- Cythonized using Cython 3.0.11 to get Python 3.13 support. +- Misc. CI and readthedocs related changes. + + +Release 1.5.0 (2023-08-08) +========================== + +- Note: this is first pyfuse3 release supporting the Cython 3.0.0 release. +- Cythonized using Cython 3.0.0 release. +- Drop Python 3.5, 3.6, 3.7 support, see #69. + Minimum requirement is Python 3.8 now. +- Get rid of PyEval_InitThreads, #55. +- CI: also test on python 3.12 / cython 3.0 release +- Tell Cython that callbacks may raise exceptions, #90. +- Misc. CI, testing, build related fixes/improvements. + + +Release 1.4.4 (2023-05-21) +========================== + +- CI: use the matrix for cy/py combinations, support and test on Cython 3 beta +- cy3: cdef void* f(void* d) noexcept with gil, #78 +- cy3: cdef nogil -> noexcept nogil +- tests: use shutil.which() instead of which(1) executable +- tests/examples: fix tmpfs: backport fix from pyfuse3 +- tests/examples: fix lltest: add statfs implementation, remove -l from umount + +Release 1.4.3 (2023-05-09) +========================== + +* cythonize with Cython 0.29.34 (brings python 3.12 support) +* also test on python 3.12-dev +* add a minimal pyproject.toml, #70 +* fix basedir in setup.py (malfunctioned with pip install -e .) +* tests: fix integer overflow on 32-bit architectures + +Release 1.4.2 (2022-05-31) +========================== + +* cythonize with Cython 0.29.30 (brings python 3.11 support) +* also test on python 3.10 and 3.11-dev +* remove "nonempty" default mount option, seems unsupported now. + +Release 1.4.1 (2021-01-31) +========================== + +* timestamp rounding tests: avoid y2038 issue in test + +Release 1.4.0 (2021-01-24) +========================== + +* Remove py2 and py3<3.5 support, minimum requirement is Python 3.5 now. + If you are stuck on Python 2.x or < 3.5, use llfuse<1.4.0. +* setup.py: return rc=2 in error cases, fixes #52. + implements same behaviour as pyfuse3 for these cases. +* Use EACCES instead of EPERM for file permission errors, fixes #36. +* Fix long-standing rounding error in file date handling when the nanosecond + part of file dates were > 999999500, fixes #38. +* Docs: add link to pyfuse3 porting hints ticket +* Testing: + + - Add Power support (ppc64le) to travis CI. + - Move CI to GitHub Actions, except ppc64le. + - Test fixes for pytest 6. Release 1.3.8 (2020-10-10) ========================== @@ -27,15 +116,14 @@ Release 1.3.6 (2019-02-14) * No change upload. * Python-LLFUSE is no longer actively maintained. Unless you are stuck - with Python 2.x or libfuse 2.x, we recommended to use the pyfuse3_ + with Python 2.x or libfuse 2.x, we recommended to use the pyfuse3 module instead. - Release 1.3.5 (2018-08-30) ========================== -* Add `handle_signals` option to `llfuse.main` -* Several fixes to `examples/passthroughfs.py` +* Add ``handle_signals`` option to ``llfuse.main`` +* Several fixes to ``examples/passthroughfs.py`` * Now compatible with Python 3.7 Release 1.3.4 (2018-04-29) diff --git a/Include/fuse_lowlevel.pxd b/Include/fuse_lowlevel.pxd index 63ffcbe..ea23ee0 100644 --- a/Include/fuse_lowlevel.pxd +++ b/Include/fuse_lowlevel.pxd @@ -54,66 +54,69 @@ cdef extern from "" nogil: int FUSE_SET_ATTR_ATIME_NOW int FUSE_SET_ATTR_MTIME_NOW + # Request handlers + # We allow these functions to raise exceptions because we will catch them + # when checking exception status on return from fuse_session_process_buf(). struct fuse_lowlevel_ops: - void (*init) (void *userdata, fuse_conn_info *conn) - void (*destroy) (void *userdata) - void (*lookup) (fuse_req_t req, fuse_ino_t parent, const_char *name) - void (*forget) (fuse_req_t req, fuse_ino_t ino, ulong_t nlookup) + void (*init) (void *userdata, fuse_conn_info *conn) except * + void (*destroy) (void *userdata) except * + void (*lookup) (fuse_req_t req, fuse_ino_t parent, const_char *name) except * + void (*forget) (fuse_req_t req, fuse_ino_t ino, ulong_t nlookup) except * void (*getattr) (fuse_req_t req, fuse_ino_t ino, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*setattr) (fuse_req_t req, fuse_ino_t ino, struct_stat *attr, - int to_set, fuse_file_info *fi) - void (*readlink) (fuse_req_t req, fuse_ino_t ino) + int to_set, fuse_file_info *fi) except * + void (*readlink) (fuse_req_t req, fuse_ino_t ino) except * void (*mknod) (fuse_req_t req, fuse_ino_t parent, const_char *name, - mode_t mode, dev_t rdev) + mode_t mode, dev_t rdev) except * void (*mkdir) (fuse_req_t req, fuse_ino_t parent, const_char *name, - mode_t mode) - void (*unlink) (fuse_req_t req, fuse_ino_t parent, const_char *name) - void (*rmdir) (fuse_req_t req, fuse_ino_t parent, const_char *name) + mode_t mode) except * + void (*unlink) (fuse_req_t req, fuse_ino_t parent, const_char *name) except * + void (*rmdir) (fuse_req_t req, fuse_ino_t parent, const_char *name) except * void (*symlink) (fuse_req_t req, const_char *link, fuse_ino_t parent, - const_char *name) + const_char *name) except * void (*rename) (fuse_req_t req, fuse_ino_t parent, const_char *name, - fuse_ino_t newparent, const_char *newname) + fuse_ino_t newparent, const_char *newname) except * void (*link) (fuse_req_t req, fuse_ino_t ino, fuse_ino_t newparent, - const_char *newname) + const_char *newname) except * void (*open) (fuse_req_t req, fuse_ino_t ino, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*read) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*write) (fuse_req_t req, fuse_ino_t ino, const_char *buf, - size_t size, off_t off, fuse_file_info *fi) + size_t size, off_t off, fuse_file_info *fi) except * void (*flush) (fuse_req_t req, fuse_ino_t ino, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*release) (fuse_req_t req, fuse_ino_t ino, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*fsync) (fuse_req_t req, fuse_ino_t ino, int datasync, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*opendir) (fuse_req_t req, fuse_ino_t ino, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*readdir) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*releasedir) (fuse_req_t req, fuse_ino_t ino, - fuse_file_info *fi) + fuse_file_info *fi) except * void (*fsyncdir) (fuse_req_t req, fuse_ino_t ino, int datasync, - fuse_file_info *fi) - void (*statfs) (fuse_req_t req, fuse_ino_t ino) + fuse_file_info *fi) except * + void (*statfs) (fuse_req_t req, fuse_ino_t ino) except * void (*setxattr) (fuse_req_t req, fuse_ino_t ino, const_char *name, - const_char *value, size_t size, int flags) + const_char *value, size_t size, int flags) except * void (*getxattr) (fuse_req_t req, fuse_ino_t ino, const_char *name, - size_t size) - void (*listxattr) (fuse_req_t req, fuse_ino_t ino, size_t size) - void (*removexattr) (fuse_req_t req, fuse_ino_t ino, const_char *name) - void (*access) (fuse_req_t req, fuse_ino_t ino, int mask) + size_t size) except * + void (*listxattr) (fuse_req_t req, fuse_ino_t ino, size_t size) except * + void (*removexattr) (fuse_req_t req, fuse_ino_t ino, const_char *name) except * + void (*access) (fuse_req_t req, fuse_ino_t ino, int mask) except * void (*create) (fuse_req_t req, fuse_ino_t parent, const_char *name, - mode_t mode, fuse_file_info *fi) + mode_t mode, fuse_file_info *fi) except * void (*write_buf) (fuse_req_t req, fuse_ino_t ino, fuse_bufvec *bufv, - off_t off, fuse_file_info *fi) + off_t off, fuse_file_info *fi) except * void (*retrieve_reply) (fuse_req_t req, void *cookie, fuse_ino_t ino, - off_t offset, fuse_bufvec *bufv) + off_t offset, fuse_bufvec *bufv) except * void (*forget_multi) (fuse_req_t req, size_t count, - fuse_forget_data *forgets) + fuse_forget_data *forgets) except * void (*fallocate) (fuse_req_t req, fuse_ino_t ino, int mode, - off_t offset, off_t length, fuse_file_info *fi) + off_t offset, off_t length, fuse_file_info *fi) except * int fuse_reply_err(fuse_req_t req, int err) void fuse_reply_none(fuse_req_t req) @@ -164,7 +167,7 @@ cdef extern from "" nogil: int fuse_session_receive_buf(fuse_session *se, fuse_buf *buf, fuse_chan **chp) void fuse_session_process_buf(fuse_session *se, - fuse_buf *buf, fuse_chan *ch) + fuse_buf *buf, fuse_chan *ch) except * void fuse_session_remove_chan(fuse_chan *ch) void fuse_session_reset(fuse_session *se) void fuse_session_exit(fuse_session *se) diff --git a/MANIFEST.in b/MANIFEST.in index a7e080f..13f07bb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,7 @@ graft rst graft util graft test prune test/.cache +prune test/.pytest_cache exclude MANIFEST.in recursive-include src *.pyx *.pxi *.c *.h global-exclude *.pyc diff --git a/README.rst b/README.rst index 42060ae..46ec1cc 100644 --- a/README.rst +++ b/README.rst @@ -9,11 +9,12 @@ The Python-LLFUSE Module .. start-intro -**Warning - no longer maintained** +**Warning - no longer developed!** -Python-LLFUSE is no longer actively maintained. Unless you are stuck -with Python 2.x or libfuse 2.x, we recommended to use the pyfuse3_ -module instead. +Python-LLFUSE is no longer actively developed and just receiving +community-contributed maintenance to keep it alive for some time. + +A good alternative for some use cases might be `mfusepy `_. Python-LLFUSE is a set of Python bindings for the low level FUSE_ API. It requires at least FUSE 2.8.0 and supports both Python 2.x and @@ -25,8 +26,6 @@ can be `read online`__ and is also included in the ``doc/html`` directory of the Python-LLFUSE tarball. -.. _pyfuse3: https://github.com/libfuse/pyfuse3 - Getting Help ------------ @@ -41,7 +40,7 @@ Contributing The Python-LLFUSE source code is available on GitHub_. -.. __: http://www.rath.org/llfuse-docs/ +.. __: https://llfuse.readthedocs.io/ .. _FUSE: http://github.com/libfuse/libfuse .. _FUSE mailing list: https://lists.sourceforge.net/lists/listinfo/fuse-devel .. _issue tracker: https://github.com/python-llfuse/python-llfuse/issues diff --git a/developer-notes/release-process.md b/developer-notes/release-process.md index b12939b..80d607f 100644 --- a/developer-notes/release-process.md +++ b/developer-notes/release-process.md @@ -6,17 +6,15 @@ # Releasing a new version # * `export DEVELOPER_MODE=0` # or just not have it set - * Bump version in `setup.py` - * Add release date to `Changes.txt` - * Check `hg status -u`, if necessary run `hg purge` to avoid undesired files in the tarball. + * Bump version in `setup.py` and version/release in `rst/conf.py` + * Add release date to `Changes.rst` + * Check `git status` to avoid undesired files in the tarball. * `./setup.py build_cython` * `./setup.py sdist` * Extract tarball in temporary directory, - * `python3 setup.py build_ext --inplace && python3 -m pytest test` - * `python setup.py build_ext --inplace && python -m pytest test` + * `./setup.py build_ext --inplace && python3 -m pytest test` * Run tests under valgrind. Build python `--with-valgrind --with-pydebug`, then `valgrind --trace-children=yes "--trace-children-skip=*mount*" python-dbg -m pytest test/` - * `./setup.py build_sphinx` - * `./setup.py upload_docs` + * `sphinx-build -b html rst doc/html` * `./util/sdist-sign 1.2.3` # use real version number, have GPG ready * `./util/upload-pypi 1.2.3` # use real version number, have twine installed * git commit, git tag -s diff --git a/examples/lltest.py b/examples/lltest.py index 44bc53c..d090d61 100755 --- a/examples/lltest.py +++ b/examples/lltest.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- ''' lltest.py - Example file system for Python-LLFUSE. @@ -23,7 +22,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' -from __future__ import division, print_function, absolute_import import os import sys @@ -52,7 +50,7 @@ class TestFs(llfuse.Operations): def __init__(self): - super(TestFs, self).__init__() + super().__init__() self.hello_name = b"message" self.hello_inode = llfuse.ROOT_INODE+1 self.hello_data = b"hello world\n" @@ -93,13 +91,31 @@ def readdir(self, fh, off): # only one entry if off == 0: - yield (self.hello_name, self.getattr(self.hello_inode), 1) + yield (self.hello_name, self.getattr(self.hello_inode), self.hello_inode) + + def statfs(self, ctx): + stat_ = llfuse.StatvfsData() + + stat_.f_bsize = 512 + stat_.f_frsize = 512 + + size = 1024 * stat_.f_frsize + stat_.f_blocks = size // stat_.f_frsize + stat_.f_bfree = max(size // stat_.f_frsize, 1024) + stat_.f_bavail = stat_.f_bfree + + inodes = 100 + stat_.f_files = inodes + stat_.f_ffree = max(inodes, 100) + stat_.f_favail = stat_.f_ffree + + return stat_ def open(self, inode, flags, ctx): if inode != self.hello_inode: raise llfuse.FUSEError(errno.ENOENT) if flags & os.O_RDWR or flags & os.O_WRONLY: - raise llfuse.FUSEError(errno.EPERM) + raise llfuse.FUSEError(errno.EACCES) return inode def read(self, fh, off, size): diff --git a/examples/tmpfs.py b/examples/tmpfs.py index 0ff052b..553fd4d 100755 --- a/examples/tmpfs.py +++ b/examples/tmpfs.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- ''' tmpfs.py - Example file system for Python-LLFUSE. @@ -22,7 +21,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' -from __future__ import division, print_function, absolute_import import os import sys @@ -53,12 +51,6 @@ log = logging.getLogger() -# For Python 2 + 3 compatibility -if sys.version_info[0] == 2: - def next(it): - return it.next() -else: - buffer = memoryview class Operations(llfuse.Operations): '''An example filesystem that stores all data in memory @@ -74,7 +66,7 @@ class Operations(llfuse.Operations): def __init__(self): - super(Operations, self).__init__() + super().__init__() self.db = sqlite3.connect(':memory:') self.db.text_factory = str self.db.row_factory = sqlite3.Row @@ -154,7 +146,10 @@ def lookup(self, inode_p, name, ctx=None): def getattr(self, inode, ctx=None): - row = self.get_row('SELECT * FROM inodes WHERE id=?', (inode,)) + try: + row = self.get_row('SELECT * FROM inodes WHERE id=?', (inode,)) + except NoSuchRowError: + raise(llfuse.FUSEError(errno.ENOENT)) entry = llfuse.EntryAttributes() entry.st_ino = inode @@ -266,8 +261,8 @@ def _replace(self, inode_p_old, name_old, inode_p_new, name_new, def link(self, inode, new_inode_p, new_name, ctx): entry_p = self.getattr(new_inode_p) if entry_p.st_nlink == 0: - log.warn('Attempted to create entry %s with unlinked parent %d', - new_name, new_inode_p) + log.warning('Attempted to create entry %s with unlinked parent %d', + new_name, new_inode_p) raise FUSEError(errno.EINVAL) self.cursor.execute("INSERT INTO contents (name, inode, parent_inode) VALUES(?,?,?)", @@ -286,7 +281,7 @@ def setattr(self, inode, attr, fields, fh, ctx): else: data = data[:attr.st_size] self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?', - (buffer(data), attr.st_size, inode)) + (memoryview(data), attr.st_size, inode)) if fields.update_mode: self.cursor.execute('UPDATE inodes SET mode=? WHERE id=?', (attr.st_mode, inode)) @@ -354,8 +349,8 @@ def create(self, inode_parent, name, mode, flags, ctx): def _create(self, inode_p, name, mode, ctx, rdev=0, target=None): if self.getattr(inode_p).st_nlink == 0: - log.warn('Attempted to create entry %s with unlinked parent %d', - name, inode_p) + log.warning('Attempted to create entry %s with unlinked parent %d', + name, inode_p) raise FUSEError(errno.EINVAL) now_ns = int(time() * 1e9) @@ -381,7 +376,7 @@ def write(self, fh, offset, buf): data = data[:offset] + buf + data[offset+len(buf):] self.cursor.execute('UPDATE inodes SET data=?, size=? WHERE id=?', - (buffer(data), len(data), fh)) + (memoryview(data), len(data), fh)) return len(buf) def release(self, fh): diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ef5db1b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools >= 78.1.1"] +build-backend = "setuptools.build_meta" diff --git a/requirements.d/rtd.txt b/requirements.d/rtd.txt new file mode 100644 index 0000000..f6629e0 --- /dev/null +++ b/requirements.d/rtd.txt @@ -0,0 +1 @@ +cython diff --git a/rst/conf.py b/rst/conf.py index 2290db6..8c5b968 100644 --- a/rst/conf.py +++ b/rst/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Python-LLFUSE documentation build configuration file, created by # sphinx-quickstart on Sat Oct 16 14:14:40 2010. @@ -14,7 +13,12 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +import os, sys +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +# we need util/ so the sphinx_cython extension is found: +sys.path.insert(0, os.path.join(basedir, 'util')) +# we need src/ also, it is needed so llfuse can be imported to generate api docs: +sys.path.insert(0, os.path.join(basedir, 'src')) #pylint: disable-all #@PydevCodeAnalysisIgnore @@ -47,17 +51,17 @@ nitpicky = True # General information about the project. -project = u'Python-LLFUSE' -copyright = u'2010-2015, Nikolaus Rath' +project = 'Python-LLFUSE' +copyright = '2010-2025, Nikolaus Rath' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.0' +version = '1.6.0' # The full version, including alpha/beta/rc tags. -release = '1.0' +release = version + '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -70,7 +74,7 @@ #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -unused_docs = [ ] +unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. @@ -185,8 +189,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'llfuse.tex', u'Python-LLFUSE Documentation', - u'Nikolaus Rath', 'manual'), + ('index', 'llfuse.tex', 'Python-LLFUSE Documentation', + 'Nikolaus Rath', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of diff --git a/rst/install.rst b/rst/install.rst index f1d656e..ad8220f 100644 --- a/rst/install.rst +++ b/rst/install.rst @@ -62,13 +62,13 @@ Development Version If you have checked out the unstable development version from the repository, a bit more effort is required. You need to also have -Cython_ (0.29.21 or newer) and Sphinx_ (1.1 or newer) installed, and the +Cython_ (Version >= 3) and Sphinx_ (1.1 or newer) installed, and the necessary commands are:: python setup.py build_cython python setup.py build_ext --inplace python -m pytest test/ - python setup.py build_sphinx + sphinx-build -b html rst doc/html python setup.py install diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a3f8b20..0000000 --- a/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[build_sphinx] -source-dir = rst -build-dir = doc diff --git a/setup.py b/setup.py index 97ad917..46cc313 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ -#!/usr/bin/env python -#-*- coding: us-ascii -*- +#!/usr/bin/env python3 ''' setup.py @@ -11,7 +10,6 @@ the terms of the GNU LGPL. ''' -from __future__ import division, print_function, absolute_import import sys import os @@ -34,16 +32,10 @@ module = sys.modules['Cython.Distutils.build_ext'] del module.build_ext -try: - import setuptools -except ImportError: - raise SystemExit('Setuptools package not found. Please install from ' - 'https://pypi.python.org/pypi/setuptools') +import setuptools from setuptools import Extension -from distutils.version import LooseVersion -# Add util to load path -basedir = os.path.abspath(os.path.dirname(sys.argv[0])) +basedir = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, os.path.join(basedir, 'util')) # when running with DEVELOPER_MODE=1 in the environment: @@ -52,26 +44,14 @@ if DEVELOPER_MODE: print('running in developer mode') warnings.resetwarnings() - # We can't use `error`, because e.g. Sphinx triggers a - # DeprecationWarning. warnings.simplefilter('default') -# Add src to load path, important for Sphinx autodoc -# to work properly -sys.path.insert(0, os.path.join(basedir, 'src')) -LLFUSE_VERSION = '1.3.8' +LLFUSE_VERSION = '1.6.0' def main(): - try: - from sphinx.application import Sphinx #pylint: disable-msg=W0612 - except ImportError: - pass - else: - fix_docutils() - - with open(os.path.join(basedir, 'README.rst'), 'r') as fh: + with open(os.path.join(basedir, 'README.rst')) as fh: long_desc = fh.read() compile_args = pkg_config('fuse', cflags=True, ldflags=False, min_ver='2.8.0') @@ -107,14 +87,6 @@ def main(): # accident. compile_args.append('-Werror=sign-compare') - # http://bugs.python.org/issue7576 - if sys.version_info[0] == 3 and sys.version_info[1] < 2: - compile_args.append('-Wno-error=missing-field-initializers') - - # http://bugs.python.org/issue969718 - if sys.version_info[0] == 2: - compile_args.append('-fno-strict-aliasing') - link_args = pkg_config('fuse', cflags=False, ldflags=True, min_ver='2.8.0') link_args.append('-lpthread') c_sources = ['src/llfuse.c', 'src/lock.c'] @@ -124,10 +96,6 @@ def main(): elif os.uname()[0] == 'Darwin': c_sources.append('src/darwin_compat.c') - install_requires = [] - if sys.version_info[0] == 2: - install_requires.append('contextlib2') - setuptools.setup( name='llfuse', zip_safe=True, @@ -137,20 +105,19 @@ def main(): author='Nikolaus Rath', author_email='Nikolaus@rath.org', url='https://github.com/python-llfuse/python-llfuse/', - license='LGPL', + license='LGPL-2.0-or-later', + license_files=['LICENSE'], classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Filesystems', - 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 'Operating System :: POSIX :: Linux', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: BSD :: FreeBSD'], @@ -158,18 +125,12 @@ def main(): keywords=['FUSE', 'python' ], package_dir={'': 'src'}, packages=setuptools.find_packages('src'), + python_requires='>=3.10', provides=['llfuse'], ext_modules=[Extension('llfuse', c_sources, extra_compile_args=compile_args, extra_link_args=link_args)], - cmdclass={'upload_docs': upload_docs, - 'build_cython': build_cython }, - command_options={ - 'build_sphinx': { - 'version': ('setup.py', LLFUSE_VERSION), - 'release': ('setup.py', LLFUSE_VERSION), - }}, - install_requires=install_requires, + cmdclass={'build_cython': build_cython}, ) @@ -184,7 +145,7 @@ def pkg_config(pkg, cflags=True, ldflags=False, min_ver=None): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) version = proc.communicate()[0].strip() if not version: - raise SystemExit() # pkg-config generates error message already + raise SystemExit(2) # pkg-config generates error message already else: raise SystemExit('%s version too old (found: %s, required: %s)' % (pkg, version, min_ver)) @@ -199,26 +160,11 @@ def pkg_config(pkg, cflags=True, ldflags=False, min_ver=None): cflags = proc.stdout.readline().rstrip() proc.stdout.close() if proc.wait() != 0: - raise SystemExit() # pkg-config generates error message already + raise SystemExit(2) # pkg-config generates error message already return cflags.decode('us-ascii').split() -class upload_docs(setuptools.Command): - user_options = [] - boolean_options = [] - description = "Upload documentation" - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - subprocess.check_call(['rsync', '-aHv', '--del', os.path.join(basedir, 'doc', 'html') + '/', - 'ebox.rath.org:/srv/www.rath.org/llfuse-docs/']) - class build_cython(setuptools.Command): user_options = [] boolean_options = [] @@ -237,11 +183,7 @@ def run(self): stderr=subprocess.STDOUT) except OSError: raise SystemExit('Cython needs to be installed for this command') - - hit = re.match('^Cython version (.+)$', version) - if not hit or LooseVersion(hit.group(1)) < "0.29": - # in fact, we need a very recent Cython version (like 0.29.21) to support py39 - raise SystemExit('Need Cython 0.29 or newer, found ' + version) + print(f"Using {version.strip()}.") cmd = ['cython', '-Wextra', '--force', '-3', '--fast-fail', '--directive', 'embedsignature=True', '--include-dir', @@ -262,30 +204,6 @@ def run(self): if subprocess.call(cmd + [path + '.pyx']) != 0: raise SystemExit('Cython compilation failed') -def fix_docutils(): - '''Work around https://bitbucket.org/birkenfeld/sphinx/issue/1154/''' - - import docutils.parsers - from docutils.parsers import rst - old_getclass = docutils.parsers.get_parser_class - - # Check if bug is there - try: - old_getclass('rst') - except AttributeError: - pass - else: - return - - def get_parser_class(parser_name): - """Return the Parser class from the `parser_name` module.""" - if parser_name in ('rst', 'restructuredtext'): - return rst.Parser - else: - return old_getclass(parser_name) - docutils.parsers.get_parser_class = get_parser_class - - assert docutils.parsers.get_parser_class('rst') is rst.Parser if __name__ == '__main__': main() diff --git a/src/darwin_compat.c b/src/darwin_compat.c index c9cbafe..4daf634 100644 --- a/src/darwin_compat.c +++ b/src/darwin_compat.c @@ -10,6 +10,11 @@ #include #include +static void _unlock_mutex(void *mutex) +{ + pthread_mutex_unlock((pthread_mutex_t *)mutex); +} + /* * Semaphore implementation based on: * @@ -152,7 +157,7 @@ darwin_sem_timedwait(darwin_sem_t *sem, const struct timespec *abs_timeout) return -1; } - pthread_cleanup_push((void(*)(void*))&pthread_mutex_unlock, + pthread_cleanup_push(&_unlock_mutex, &sem->__data.local.count_lock); pthread_mutex_lock(&sem->__data.local.count_lock); @@ -213,7 +218,7 @@ darwin_sem_wait(darwin_sem_t *sem) /* Must be volatile or will be clobbered by longjmp */ volatile int res = 0; - pthread_cleanup_push((void(*)(void*))&pthread_mutex_unlock, + pthread_cleanup_push(&_unlock_mutex, &sem->__data.local.count_lock); pthread_mutex_lock(&sem->__data.local.count_lock); diff --git a/src/fuse_api.pxi b/src/fuse_api.pxi index a748cca..fa731a6 100644 --- a/src/fuse_api.pxi +++ b/src/fuse_api.pxi @@ -195,12 +195,8 @@ def getxattr(path, name, size_t size_guess=128, namespace='user'): finally: stdlib.free(buf) -if os.uname()[0] == 'Darwin': - default_options = frozenset(('big_writes', 'default_permissions', - 'no_splice_read', 'splice_write', 'splice_move')) -else: - default_options = frozenset(('big_writes', 'nonempty', 'default_permissions', - 'no_splice_read', 'splice_write', 'splice_move')) +default_options = frozenset(('big_writes', 'default_permissions', + 'no_splice_read', 'splice_write', 'splice_move')) def init(ops, mountpoint, options=default_options): '''Initialize and mount FUSE file system @@ -276,7 +272,7 @@ def main(workers=None, handle_signals=True): and the function to return. *SIGINT* (Ctrl-C) will thus *not* result in a `KeyboardInterrupt` exception while this function is runnning. Note setting *handle_signals* to `False` means you must handle the signals - by yourself and call `stop` to make the `main` returns. + by yourself and call ``stop`` to make the `main` returns. When the function returns because the file system has received an unmount request it will return `None`. If it returns because it has received a @@ -324,13 +320,7 @@ def main(workers=None, handle_signals=True): tmp = exc_info exc_info = None - # The explicit version check works around a Cython bug with - # the 3-parameter version of the raise statement, c.f. - # https://github.com/cython/cython/commit/a6195f1a44ab21f5aa4b2a1b1842dd93115a3f42 - if PY_MAJOR_VERSION < 3: - raise tmp[0], tmp[1], tmp[2] - else: - raise tmp[1].with_traceback(tmp[2]) + raise tmp[1].with_traceback(tmp[2]) if exit_reason == signal.SIGKILL: return None @@ -382,7 +372,7 @@ ctypedef struct worker_data_t: void* buf size_t bufsize -cdef void* worker_start(void* data) with gil: +cdef void* worker_start(void* data) noexcept with gil: cdef worker_data_t *wd cdef int res cdef uintptr_t tid @@ -397,7 +387,7 @@ cdef void* worker_start(void* data) with gil: session_loop(wd.buf, wd.bufsize) except: fuse_session_exit(session) - tid = wd.thread_id + tid = wd.thread_id log.error('FUSE worker thread %d terminated with exception, ' 'aborting processing', tid) res = pthread_mutex_lock(&exc_info_mutex) @@ -434,7 +424,6 @@ cdef session_loop_mt(workers): sigaddset(&newset, signal.SIGHUP); sigaddset(&newset, signal.SIGQUIT); - PyEval_InitThreads() bufsize = fuse_chan_bufsize(channel) wd = calloc_or_raise(workers, sizeof(worker_data_t)) try: @@ -519,13 +508,7 @@ def close(unmount=True): tmp = exc_info exc_info = None - # The explicit version check works around a Cython bug with - # the 3-parameter version of the raise statement, c.f. - # https://github.com/cython/cython/commit/a6195f1a44ab21f5aa4b2a1b1842dd93115a3f42 - if PY_MAJOR_VERSION < 3: - raise tmp[0], tmp[1], tmp[2] - else: - raise tmp[1].with_traceback(tmp[2]) + raise tmp[1].with_traceback(tmp[2]) def invalidate_inode(fuse_ino_t inode, attr_only=False): '''Invalidate cache for *inode* diff --git a/src/handlers.pxi b/src/handlers.pxi index 24604d7..a54d5bc 100644 --- a/src/handlers.pxi +++ b/src/handlers.pxi @@ -694,7 +694,7 @@ cdef void fuse_access (fuse_req_t req, fuse_ino_t ino, int mask) with gil: if allowed: ret = fuse_reply_err(req, 0) else: - ret = fuse_reply_err(req, EPERM) + ret = fuse_reply_err(req, EACCES) except FUSEError as e: ret = fuse_reply_err(req, e.errno) except: diff --git a/src/llfuse.h b/src/llfuse.h index 6e0b5e0..c6c2061 100644 --- a/src/llfuse.h +++ b/src/llfuse.h @@ -14,7 +14,7 @@ the terms of the GNU LGPL. #ifdef __linux__ #define PLATFORM PLATFORM_LINUX -#elif __FreeBSD_kernel__&&__GLIBC__ +#elif __FreeBSD_kernel__ && __GLIBC__ #define PLATFORM PLATFORM_LINUX #elif __FreeBSD__ #define PLATFORM PLATFORM_BSD diff --git a/src/llfuse.pyx b/src/llfuse.pyx index bd9bc68..a8f5597 100644 --- a/src/llfuse.pyx +++ b/src/llfuse.pyx @@ -25,7 +25,7 @@ from posix.types cimport mode_t, dev_t, off_t from libc.stdint cimport uint32_t from libc.stdlib cimport const_char from libc cimport stdlib, string, errno, dirent -from libc.errno cimport ETIMEDOUT, EPROTO, EINVAL, EPERM, ENOMSG, ENOATTR +from libc.errno cimport EACCES, ETIMEDOUT, EPROTO, EINVAL, EPERM, ENOMSG, ENOATTR from posix.unistd cimport getpid from posix.time cimport timespec from posix.signal cimport (sigemptyset, sigaddset, SIG_BLOCK, SIG_SETMASK, @@ -36,8 +36,8 @@ from cpython.buffer cimport (PyObject_GetBuffer, PyBuffer_Release, PyBUF_CONTIG_RO, PyBUF_CONTIG) cimport cpython.exc cimport cython -from cpython.version cimport PY_MAJOR_VERSION from libc cimport signal +import contextlib ###################### @@ -95,7 +95,6 @@ cdef extern from *: EDEADLK cdef extern from "Python.h" nogil: - void PyEval_InitThreads() int PY_SSIZE_T_MAX # Actually passed as -D to cc (and defined in setup.py) @@ -112,15 +111,8 @@ import sys import os.path import threading from pickle import PicklingError - -if PY_MAJOR_VERSION < 3: - from Queue import Queue - import contextlib2 as contextlib - str_t = bytes -else: - from queue import Queue - str_t = str - import contextlib +from queue import Queue +str_t = str ################## # GLOBAL VARIABLES diff --git a/src/misc.pxi b/src/misc.pxi index bd25cda..79a02ff 100644 --- a/src/misc.pxi +++ b/src/misc.pxi @@ -10,6 +10,8 @@ This file is part of Python-LLFUSE. This work may be distributed under the terms of the GNU LGPL. ''' +_NANOS_PER_SEC = 1000000000 + cdef int handle_exc(fuse_req_t req): '''Try to call fuse_reply_err and terminate main loop''' @@ -197,7 +199,7 @@ cdef class Lock: def yield_(self, count=1): '''Yield global lock to a different thread - A call to `~Lock.yield_` is roughly similar to:: + A call to ``~Lock.yield_`` is roughly similar to:: for i in range(count): if no_threads_waiting_for(lock): @@ -205,8 +207,8 @@ cdef class Lock: lock.release() lock.acquire() - However, when using `~Lock.yield_` it is guaranteed that the lock will - actually be passed to a different thread (the above pseude-code may + However, when using ``~Lock.yield_`` it is guaranteed that the lock will + actually be passed to a different thread (the above pseudocode may result in the same thread re-acquiring the lock *count* times). ''' @@ -286,26 +288,16 @@ def _notify_loop(): cdef str2bytes(s): '''Convert *s* to bytes - Under Python 2.x, just returns *s*. Under Python 3.x, converts - to file system encoding using surrogateescape. + Converts to file system encoding using surrogateescape. ''' - - if PY_MAJOR_VERSION < 3: - return s - else: - return s.encode(fse, 'surrogateescape') + return s.encode(fse, 'surrogateescape') cdef bytes2str(s): '''Convert *s* to str - Under Python 2.x, just returns *s*. Under Python 3.x, converts - from file system encoding using surrogateescape. + Converts from file system encoding using surrogateescape. ''' - - if PY_MAJOR_VERSION < 3: - return s - else: - return s.decode(fse, 'surrogateescape') + return s.decode(fse, 'surrogateescape') cdef strerror(int errno): try: @@ -476,29 +468,29 @@ cdef class EntryAttributes: @property def st_atime_ns(self): '''Time of last access in (integer) nanoseconds''' - return (int(self.attr.st_atime) * 10**9 + GET_ATIME_NS(self.attr)) + return (int(self.attr.st_atime) * _NANOS_PER_SEC + GET_ATIME_NS(self.attr)) @st_atime_ns.setter def st_atime_ns(self, val): - self.attr.st_atime = val / 10**9 - SET_ATIME_NS(self.attr, val % 10**9) + self.attr.st_atime = val // _NANOS_PER_SEC + SET_ATIME_NS(self.attr, val % _NANOS_PER_SEC) @property def st_mtime_ns(self): '''Time of last modification in (integer) nanoseconds''' - return (int(self.attr.st_mtime) * 10**9 + GET_MTIME_NS(self.attr)) + return (int(self.attr.st_mtime) * _NANOS_PER_SEC + GET_MTIME_NS(self.attr)) @st_mtime_ns.setter def st_mtime_ns(self, val): - self.attr.st_mtime = val / 10**9 - SET_MTIME_NS(self.attr, val % 10**9) + self.attr.st_mtime = val // _NANOS_PER_SEC + SET_MTIME_NS(self.attr, val % _NANOS_PER_SEC) @property def st_ctime_ns(self): '''Time of last inode modification in (integer) nanoseconds''' - return (int(self.attr.st_ctime) * 10**9 + GET_CTIME_NS(self.attr)) + return (int(self.attr.st_ctime) * _NANOS_PER_SEC + GET_CTIME_NS(self.attr)) @st_ctime_ns.setter def st_ctime_ns(self, val): - self.attr.st_ctime = val / 10**9 - SET_CTIME_NS(self.attr, val % 10**9) + self.attr.st_ctime = val // _NANOS_PER_SEC + SET_CTIME_NS(self.attr, val % _NANOS_PER_SEC) @property def st_birthtime_ns(self): @@ -509,15 +501,15 @@ cdef class EntryAttributes: # Use C macro to prevent compiler error on Linux # (where st_birthtime does not exist) - return int(GET_BIRTHTIME(self.attr) * 10**9 + return int(GET_BIRTHTIME(self.attr) * _NANOS_PER_SEC + GET_BIRTHTIME_NS(self.attr)) @st_birthtime_ns.setter def st_birthtime_ns(self, val): # Use C macro to prevent compiler error on Linux # (where st_birthtime does not exist) - SET_BIRTHTIME(self.attr, val / 10**9) - SET_BIRTHTIME_NS(self.attr, val % 10**9) + SET_BIRTHTIME(self.attr, val // _NANOS_PER_SEC) + SET_BIRTHTIME_NS(self.attr, val % _NANOS_PER_SEC) # Pickling and copy support def __getstate__(self): @@ -693,13 +685,13 @@ cdef inline encap_ptr(void *ptr): cap.ptr = ptr return cap -cdef void signal_handler(int sig, siginfo_t *si, void* ctx) nogil: +cdef void signal_handler(int sig, siginfo_t *si, void* ctx) noexcept nogil: global exit_reason if session != NULL: fuse_session_exit(session) exit_reason = sig -cdef void do_nothing(int sig, siginfo_t *si, void* ctx) nogil: +cdef void do_nothing(int sig, siginfo_t *si, void* ctx) noexcept nogil: pass cdef int sigaction_p(int sig, sigaction_t *sa, diff --git a/test/conftest.py b/test/conftest.py index fa19c6a..9564053 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -38,7 +38,7 @@ def pytest_addoption(parser): # example, if a request handler raises an exception, the server first signals an # error to FUSE (causing the test to fail), and then logs the exception. Without # the extra delay, the exception will go into nowhere. -@pytest.mark.hookwrapper +@pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): outcome = yield failed = outcome.excinfo is not None diff --git a/test/pytest.ini b/test/pytest.ini index bc4af36..9c38d14 100644 --- a/test/pytest.ini +++ b/test/pytest.ini @@ -1,2 +1,3 @@ [pytest] addopts = --verbose --assert=rewrite --tb=native -x +markers = uses_fuse diff --git a/test/pytest_checklogs.py b/test/pytest_checklogs.py index e3aeae1..1bb9640 100644 --- a/test/pytest_checklogs.py +++ b/test/pytest_checklogs.py @@ -19,20 +19,7 @@ import sys import logging from contextlib import contextmanager -from distutils.version import LooseVersion -def pytest_configure(config): - # pytest-catchlog was integrated in pytest 3.3.0 - if (LooseVersion(pytest.__version__) < "3.3.0" and - not config.pluginmanager.hasplugin('pytest_catchlog')): - raise ImportError('pytest catchlog plugin not found') - -# Fail tests if they result in log messages of severity WARNING or more. -def check_test_log(caplog): - for record in caplog.records: - if (record.levelno >= logging.WARNING and - not getattr(record, 'checklogs_ignore', False)): - raise AssertionError('Logger received warning messages') class CountMessagesHandler(logging.Handler): def __init__(self, level=logging.NOTSET): @@ -75,16 +62,12 @@ def filter(record): logger.removeHandler(handler) if count is not None and handler.count != count: - raise AssertionError('Expected to catch %d %r messages, but got only %d' - % (count, pattern, handler.count)) + pytest.fail('Expected to catch %d %r messages, but got only %d' + % (count, pattern, handler.count)) def check_test_output(capfd, item): (stdout, stderr) = capfd.readouterr() - # Write back what we've read (so that it will still be printed) - sys.stdout.write(stdout) - sys.stderr.write(stderr) - # Strip out false positives try: false_pos = item.checklogs_fp @@ -101,10 +84,10 @@ def check_test_output(capfd, item): cp = re.compile(r'\b{}\b'.format(pattern), re.IGNORECASE | re.MULTILINE) hit = cp.search(stderr) if hit: - raise AssertionError('Suspicious output to stderr (matched "%s")' % hit.group(0)) + pytest.fail('Suspicious output to stderr (matched "%s")' % hit.group(0)) hit = cp.search(stdout) if hit: - raise AssertionError('Suspicious output to stdout (matched "%s")' % hit.group(0)) + pytest.fail('Suspicious output to stdout (matched "%s")' % hit.group(0)) def register_output(item, pattern, count=1, flags=re.MULTILINE): '''Register *pattern* as false positive for output checking @@ -121,21 +104,14 @@ def reg_output(request): request.node.checklogs_fp = [] return functools.partial(register_output, request.node) -def check_output(item): - pm = item.config.pluginmanager - cm = pm.getplugin('capturemanager') - capmethod = (getattr(cm, '_capturing', None) or - getattr(item, '_capture_fixture', None) or - getattr(cm, '_global_capturing', None)) - check_test_output(capmethod, item) - check_test_log(item.catch_log_handler) - -@pytest.hookimpl(trylast=True) -def pytest_runtest_setup(item): - check_output(item) -@pytest.hookimpl(trylast=True) -def pytest_runtest_call(item): - check_output(item) -@pytest.hookimpl(trylast=True) -def pytest_runtest_teardown(item, nextitem): - check_output(item) +# Autouse fixtures are instantiated before explicitly used fixtures, this should also +# catch log messages emitted when e.g. initializing resources in other fixtures. +@pytest.fixture(autouse=True) +def check_output(caplog, capfd, request): + yield + for when in ("setup", "call", "teardown"): + for record in caplog.get_records(when): + if (record.levelno >= logging.WARNING and + not getattr(record, 'checklogs_ignore', False)): + pytest.fail('Logger received warning messages.') + check_test_output(capfd, request.node) diff --git a/test/test_api.py b/test/test_api.py index cc90c3a..8ff84d4 100755 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- ''' test_api.py - Unit tests for Python-LLFUSE. @@ -9,7 +8,6 @@ the terms of the GNU LGPL. ''' -from __future__ import division, print_function, absolute_import if __name__ == '__main__': import pytest @@ -78,7 +76,7 @@ def test_xattr(): llfuse.setxattr(fh.name, key, value) except OSError as exc: if exc.errno == errno.ENOTSUP: - pytest.skip('ACLs not supported for %s' % fh.name) + pytest.skip('xattrs not supported for %s' % fh.name) raise assert _getxattr_helper(fh.name, key) == value diff --git a/test/test_examples.py b/test/test_examples.py index d4c75ed..ffd09ab 100755 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- ''' test_examples.py - Unit tests for Python-LLFUSE. @@ -9,7 +8,6 @@ the terms of the GNU LGPL. ''' -from __future__ import division, print_function, absolute_import if __name__ == '__main__': import pytest @@ -26,6 +24,7 @@ import errno from tempfile import NamedTemporaryFile from util import fuse_test_marker, wait_for_mount, umount, cleanup +from llfuse import _NANOS_PER_SEC basename = os.path.join(os.path.dirname(__file__), '..') TEST_FILE = __file__ @@ -50,11 +49,11 @@ def test_lltest(tmpdir): wait_for_mount(mount_process, mnt_dir) assert os.listdir(mnt_dir) == [ 'message' ] filename = os.path.join(mnt_dir, 'message') - with open(filename, 'r') as fh: + with open(filename) as fh: assert fh.read() == 'hello world\n' with pytest.raises(IOError) as exc_info: open(filename, 'r+') - assert exc_info.value.errno == errno.EPERM + assert exc_info.value.errno == errno.EACCES with pytest.raises(IOError) as exc_info: open(filename + 'does-not-exist', 'r+') assert exc_info.value.errno == errno.ENOENT @@ -80,6 +79,7 @@ def test_tmpfs(tmpdir): tst_chown(mnt_dir) tst_chmod(mnt_dir) tst_utimens(mnt_dir) + tst_rounding(mnt_dir) tst_link(mnt_dir) tst_readdir(mnt_dir) tst_statvfs(mnt_dir) @@ -92,8 +92,6 @@ def test_tmpfs(tmpdir): else: umount(mount_process, mnt_dir) -@pytest.mark.skipif(sys.version_info < (3,3), - reason="requires python3.3") def test_passthroughfs(tmpdir): mnt_dir = str(tmpdir.mkdir('mnt')) src_dir = str(tmpdir.mkdir('src')) @@ -113,6 +111,7 @@ def test_passthroughfs(tmpdir): tst_chmod(mnt_dir) # Underlying fs may not have full nanosecond resolution tst_utimens(mnt_dir, ns_tol=1000) + tst_rounding(mnt_dir) tst_link(mnt_dir) tst_readdir(mnt_dir) tst_statvfs(mnt_dir) @@ -267,11 +266,8 @@ def tst_readdir(mnt_dir): os.rmdir(subdir) os.rmdir(dir_) -def tst_truncate_path(mnt_dir): - if sys.version_info < (3,0): - # 2.x has no os.truncate - return +def tst_truncate_path(mnt_dir): assert len(TEST_DATA) > 1024 filename = os.path.join(mnt_dir, name_generator()) @@ -326,20 +322,47 @@ def tst_utimens(mnt_dir, ns_tol=0): atime = fstat.st_atime + 42.28 mtime = fstat.st_mtime - 42.23 - if sys.version_info < (3,3): - os.utime(filename, (atime, mtime)) - else: - atime_ns = fstat.st_atime_ns + int(42.28*1e9) - mtime_ns = fstat.st_mtime_ns - int(42.23*1e9) - os.utime(filename, None, ns=(atime_ns, mtime_ns)) + atime_ns = fstat.st_atime_ns + int(42.28*1e9) + mtime_ns = fstat.st_mtime_ns - int(42.23*1e9) + os.utime(filename, None, ns=(atime_ns, mtime_ns)) fstat = os.lstat(filename) assert abs(fstat.st_atime - atime) < 1e-3 assert abs(fstat.st_mtime - mtime) < 1e-3 - if sys.version_info >= (3,3): - assert abs(fstat.st_atime_ns - atime_ns) <= ns_tol - assert abs(fstat.st_mtime_ns - mtime_ns) <= ns_tol + assert abs(fstat.st_atime_ns - atime_ns) <= ns_tol + assert abs(fstat.st_mtime_ns - mtime_ns) <= ns_tol + + checked_unlink(filename, mnt_dir, isdir=True) + +def tst_rounding(mnt_dir, ns_tol=0): + filename = os.path.join(mnt_dir, name_generator()) + os.mkdir(filename) + fstat = os.lstat(filename) + + # Approximately 67 years, ending in 999. + # Note: 67 years were chosen to avoid y2038 issues (1970 + 67 = 2037). + # Testing these is **not** in scope of this test. + secs = 67 * 365 * 24 * 3600 + 999 + # Max nanos + nanos = _NANOS_PER_SEC - 1 + + # seconds+ns and ns_tol as a float in seconds + secs_f = secs + nanos / _NANOS_PER_SEC + secs_tol = ns_tol / _NANOS_PER_SEC + + atime_ns = secs * _NANOS_PER_SEC + nanos + mtime_ns = atime_ns + + os.utime(filename, None, ns=(atime_ns, mtime_ns)) + + fstat = os.lstat(filename) + + assert abs(fstat.st_atime - secs_f) <= secs_tol + assert abs(fstat.st_mtime - secs_f) <= secs_tol + + assert abs(fstat.st_atime_ns - atime_ns) <= ns_tol + assert abs(fstat.st_mtime_ns - mtime_ns) <= ns_tol checked_unlink(filename, mnt_dir, isdir=True) diff --git a/test/test_fs.py b/test/test_fs.py index 7bbd419..dda67d6 100755 --- a/test/test_fs.py +++ b/test/test_fs.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- ''' test_fs.py - Unit tests for Python-LLFUSE. @@ -9,7 +8,6 @@ the terms of the GNU LGPL. ''' -from __future__ import division, print_function, absolute_import import pytest import sys @@ -29,7 +27,7 @@ pytestmark = fuse_test_marker() -@pytest.yield_fixture() +@pytest.fixture def testfs(tmpdir): # We can't use forkserver because we have to make sure @@ -82,7 +80,7 @@ def check(_wait_time=[0.01]): def test_invalidate_inode(testfs): (mnt_dir, fs_state) = testfs - with open(os.path.join(mnt_dir, 'message'), 'r') as fh: + with open(os.path.join(mnt_dir, 'message')) as fh: assert fh.read() == 'hello world\n' assert fs_state.read_called fs_state.read_called = False @@ -105,7 +103,7 @@ def check(_wait_time=[0.01]): def test_notify_store(testfs): (mnt_dir, fs_state) = testfs - with open(os.path.join(mnt_dir, 'message'), 'r') as fh: + with open(os.path.join(mnt_dir, 'message')) as fh: llfuse.setxattr(mnt_dir, 'command', b'store') fs_state.read_called = False assert fh.read() == 'hello world\n' @@ -130,7 +128,7 @@ def test_entry_timeout(testfs): def test_attr_timeout(testfs): (mnt_dir, fs_state) = testfs fs_state.attr_timeout = 1 - with open(os.path.join(mnt_dir, 'message'), 'r') as fh: + with open(os.path.join(mnt_dir, 'message')) as fh: os.fstat(fh.fileno()) assert fs_state.getattr_called fs_state.getattr_called = False @@ -144,7 +142,7 @@ def test_attr_timeout(testfs): class Fs(llfuse.Operations): def __init__(self, cross_process): - super(Fs, self).__init__() + super().__init__() self.hello_name = b"message" self.hello_inode = llfuse.ROOT_INODE+1 self.hello_data = b"hello world\n" @@ -209,7 +207,7 @@ def open(self, inode, flags, ctx): if inode != self.hello_inode: raise llfuse.FUSEError(errno.ENOENT) if flags & os.O_RDWR or flags & os.O_WRONLY: - raise llfuse.FUSEError(errno.EPERM) + raise llfuse.FUSEError(errno.EACCES) return inode def read(self, fh, off, size): diff --git a/test/test_rounding.py b/test/test_rounding.py new file mode 100755 index 0000000..6fd037e --- /dev/null +++ b/test/test_rounding.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +''' +test_api.py - Unit tests for Python-LLFUSE. + +Copyright © 2020 Philip Warner + +This file is part of Python-LLFUSE. This work may be distributed under +the terms of the GNU LGPL. +''' + + +if __name__ == '__main__': + import pytest + import sys + sys.exit(pytest.main([__file__] + sys.argv[1:])) + +import llfuse +from llfuse import _NANOS_PER_SEC + +def test_rounding(): + # Incorrect division previously resulted in rounding errors for + # all dates. + entry = llfuse.EntryAttributes() + + # Approximately 67 years, ending in 999. + # Note: 67 years were chosen to avoid y2038 issues (1970 + 67 = 2037). + # Testing these is **not** in scope of this test. + secs = 67 * 365 * 24 * 3600 + 999 + nanos = _NANOS_PER_SEC - 1 + + total = secs * _NANOS_PER_SEC + nanos + + entry.st_atime_ns = total + entry.st_ctime_ns = total + entry.st_mtime_ns = total + # Birthtime skipped -- only valid under BSD and OSX + #entry.st_birthtime_ns = total + + assert entry.st_atime_ns == total + assert entry.st_ctime_ns == total + assert entry.st_mtime_ns == total + # Birthtime skipped -- only valid under BSD and OSX + #assert entry.st_birthtime_ns == total diff --git a/test/travis-install.sh b/test/travis-install.sh deleted file mode 100755 index 4d98d12..0000000 --- a/test/travis-install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -e - -# as long as we test on python 3.4, we need pytest < 5.0 -pip install 'pytest<5.0' pytest-catchlog cython sphinx -cython --version diff --git a/test/travis-test.sh b/test/travis-test.sh deleted file mode 100755 index fd20ffd..0000000 --- a/test/travis-test.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -e - -python setup.py build_cython -python setup.py build_ext --inplace -python -m pytest test/ - -python setup.py build_sphinx diff --git a/test/util.py b/test/util.py index a5c37a8..79e9dbc 100644 --- a/test/util.py +++ b/test/util.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- ''' util.py - Utility functions for Python-LLFUSE unit tests. @@ -9,19 +8,15 @@ the terms of the GNU LGPL. ''' -from __future__ import division, print_function, absolute_import import platform +import shutil import subprocess import pytest import os import stat import time -import sys -# For Python 2 + 3 compatibility -if sys.version_info[0] == 2: - subprocess.DEVNULL = open('/dev/null', 'w') def fuse_test_marker(): '''Return a pytest.marker that indicates FUSE availability @@ -36,15 +31,9 @@ def fuse_test_marker(): return pytest.mark.uses_fuse() skip = lambda x: pytest.mark.skip(reason=x) - # Python 2.x: Popen is not a context manager... - which = subprocess.Popen(['which', 'fusermount'], stdout=subprocess.PIPE, - universal_newlines=True) - try: - fusermount_path = which.communicate()[0].strip() - finally: - which.wait() + fusermount_path = shutil.which("fusermount") - if not fusermount_path or which.returncode != 0: + if fusermount_path is None: return skip("Can't find fusermount executable") if not os.path.exists('/dev/fuse'): @@ -112,7 +101,7 @@ def cleanup(mnt_dir): def umount(mount_process, mnt_dir): if platform.system() == 'Darwin': - subprocess.check_call(['umount', '-l', mnt_dir]) + subprocess.check_call(['umount', mnt_dir]) else: subprocess.check_call(['fusermount', '-z', '-u', mnt_dir]) assert not os.path.ismount(mnt_dir) diff --git a/util/sphinx_cython.py b/util/sphinx_cython.py index 5ce9f17..50f9926 100644 --- a/util/sphinx_cython.py +++ b/util/sphinx_cython.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- ''' sphinx_cython.py @@ -11,7 +10,6 @@ the terms of the GNU LGPL. ''' -from __future__ import division, print_function, absolute_import import re diff --git a/util/upload-pypi b/util/upload-pypi index b6338e4..f3d3ff1 100755 --- a/util/upload-pypi +++ b/util/upload-pypi @@ -8,11 +8,11 @@ if [ "$R" = "" ]; then fi if [ "$2" = "test" ]; then - export TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/ + export TWINE_REPOSITORY=testllfuse else - export TWINE_REPOSITORY_URL= + export TWINE_REPOSITORY=llfuse fi D=dist/llfuse-$R.tar.gz -twine upload $D.asc $D +twine upload $D