Repository: pytest-dev/pyfakefs (python)

Issue:
`os.getxattr` should raise `OSError(ENODATA)` for non-existent extended attributes
**Describe the bug**
The faked `os.getxattr` returns `None` if the extended attribute does not exist (and is documented as such). The standard `os.getxattr` either returns `bytes` or raises an `OSError` from the underlying syscall. The `getxattr` syscall fails with `ENODATA` if the attribute name is legal but is unset, and fails with `ENOTSUP` if the attribute name is illegal.

**How To Reproduce**
```python
def read_nonexistent_xattr(s: Path):
    s.write_text("file content")
    # Non-existent attribute:
    with pytest.raises(OSError) as exc:
        os.getxattr(s, "user.nonesuch_attribute")
    assert exc.value.errno == errno.ENODATA
    # Invalid attribute:
    with pytest.raises(OSError) as exc:
        os.getxattr(s, "invalid attribute name")
    assert exc.value.errno == errno.ENOTSUP

def test_realfs():
    read_nonexistent_xattr(Path("file.txt"))

def test_fakefs(fs):
    read_nonexistent_xattr(Path("file.txt"))
```

**Your environment**
Please run the following in the environment where the problem happened and
paste the output.
```console
$ python -c "import platform; print(platform.platform())"
Linux-6.7.6-arch1-1-x86_64-with-glibc2.39
$ python -c "import sys; print('Python', sys.version)"
Python 3.11.7 (main, Jan 29 2024, 16:03:57) [GCC 13.2.1 20230801]
$ python -c "from pyfakefs import __version__; print('pyfakefs', __version__)"
pyfakefs 5.4.dev0
$ python -c "import pytest; print('pytest', pytest.__version__)"
pytest 7.2.0
```


Pull request:
Fix handling of missing attribute in os.getxattr
- no tests for real OS (extended atributes not enabled in CI)
- fixes #971

#### Tasks
- [x] Unit tests added that reproduce the issue or prove feature is working
- [x] Fix or feature added
- [x] Entry to release notes added
- [x] Pre-commit CI shows no errors
- [x] Unit tests passing
- [x] For documentation changes: The Read the Docs preview builds and looks as expected


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 376b1ad..f036616 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -22,6 +22,7 @@ The released versions correspond to PyPI releases.
   (see [#965](../../issues/965))
 * fixed handling of `dirfd` in `os.symlink` (see [#968](../../issues/968))
 * add missing `follow_symlink` argument to `os.link` (see [#973](../../issues/973))
+* fixed handling of missing attribute in `os.getxattr` (see [#971](../../issues/971))
 
 ### Enhancements
 * added support for `O_NOFOLLOW` and `O_DIRECTORY` flags in `os.open`
diff --git a/README.md b/README.md
index b55c63d..ad3f997 100644
--- a/README.md
+++ b/README.md
@@ -120,7 +120,7 @@ for more information.
 pyfakefs.py was initially developed at Google by Mike Bland as a modest fake
 implementation of core Python modules.  It was introduced to all of Google
 in September 2006. Since then, it has been enhanced to extend its
-functionality and usefulness.  At last count, pyfakefs was used in over 2,000
+functionality and usefulness.  At last count, pyfakefs was used in over 20,000
 Python tests at Google.
 
 Google released pyfakefs to the public in 2011 as Google Code project
diff --git a/docs/usage.rst b/docs/usage.rst
index 0773c02..e7ba446 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -690,7 +690,7 @@ While ``pyfakefs`` can be used just with the standard Python file system
 functions, there are a few convenience methods in ``fake_filesystem`` that can
 help you setting up your tests. The methods can be accessed via the
 ``fake_filesystem`` instance in your tests: ``Patcher.fs``, the ``fs``
-fixture in pytest, ``TestCase.fs`` for ``unittest``, and the ``fs`` argument
+fixture in pytest, ``TestCase.fs`` for ``unittest``, and the positional argument
 for the ``patchfs`` decorator.
 
 File creation helpers
@@ -965,6 +965,10 @@ The following test works both under Windows and Linux:
       assert os.path.splitdrive(r"C:\foo\bar") == ("C:", r"\foo\bar")
       assert os.path.ismount("C:")
 
+.. note:: Only behavior not relying on OS-specific functionality is emulated on another system.
+  For example, if you use the Linux-specific functionality of extended attributes (``os.getxattr`` etc.)
+  in your code, you have to test this under Linux.
+
 Set file as inaccessible under Windows
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Normally, if you try to set a file or directory as inaccessible using ``chmod`` under
diff --git a/pyfakefs/fake_os.py b/pyfakefs/fake_os.py
index d2f3f24..68741a7 100644
--- a/pyfakefs/fake_os.py
+++ b/pyfakefs/fake_os.py
@@ -491,6 +491,8 @@ class FakeOsModule:
         if isinstance(attribute, bytes):
             attribute = attribute.decode(sys.getfilesystemencoding())
         file_obj = self.filesystem.resolve(path, follow_symlinks, allow_fd=True)
+        if attribute not in file_obj.xattr:
+            raise OSError(errno.ENODATA, "No data available", path)
         return file_obj.xattr.get(attribute)
 
     def listxattr(


Test patch:
diff --git a/pyfakefs/tests/fake_os_test.py b/pyfakefs/tests/fake_os_test.py
index 0240b3f..a351433 100644
--- a/pyfakefs/tests/fake_os_test.py
+++ b/pyfakefs/tests/fake_os_test.py
@@ -5496,26 +5496,25 @@ class FakeExtendedAttributeTest(FakeOsModuleTestBase):
         self.assertEqual([], self.os.listxattr(self.dir_path))
         self.assertEqual([], self.os.listxattr(self.file_path))
 
+    def test_getxattr_raises_for_non_existing_file(self):
+        with self.assertRaises(FileNotFoundError):
+            self.os.getxattr("bogus_path", "test")
+
+    def test_getxattr_raises_for_non_existing_attribute(self):
+        with self.assertRaises(OSError) as cm:
+            self.os.getxattr(self.file_path, "bogus")
+        self.assertEqual(errno.ENODATA, cm.exception.errno)
+
     def test_setxattr(self):
-        self.assertRaises(TypeError, self.os.setxattr, self.file_path, "test", "value")
-        self.assert_raises_os_error(
-            errno.EEXIST,
-            self.os.setxattr,
-            self.file_path,
-            "test",
-            b"value",
-            self.os.XATTR_REPLACE,
-        )
+        with self.assertRaises(TypeError):
+            self.os.setxattr(self.file_path, "test", "value")
+        with self.assertRaises(FileExistsError):
+            self.os.setxattr(self.file_path, "test", b"value", self.os.XATTR_REPLACE)
         self.os.setxattr(self.file_path, "test", b"value")
         self.assertEqual(b"value", self.os.getxattr(self.file_path, "test"))
-        self.assert_raises_os_error(
-            errno.ENODATA,
-            self.os.setxattr,
-            self.file_path,
-            "test",
-            b"value",
-            self.os.XATTR_CREATE,
-        )
+        with self.assertRaises(OSError) as cm:
+            self.os.setxattr(self.file_path, "test", b"value", self.os.XATTR_CREATE)
+        self.assertEqual(errno.ENODATA, cm.exception.errno)
 
     def test_removeattr(self):
         self.os.removexattr(self.file_path, "test")
@@ -5525,7 +5524,9 @@ class FakeExtendedAttributeTest(FakeOsModuleTestBase):
         self.assertEqual(b"value", self.os.getxattr(self.file_path, "test"))
         self.os.removexattr(self.file_path, "test")
         self.assertEqual([], self.os.listxattr(self.file_path))
-        self.assertIsNone(self.os.getxattr(self.file_path, "test"))
+        with self.assertRaises(OSError) as cm:
+            self.os.getxattr(self.file_path, "test")
+        self.assertEqual(errno.ENODATA, cm.exception.errno)
 
     def test_default_path(self):
         self.os.chdir(self.dir_path)