Repository: pytest-dev/pyfakefs

Issue:
Fs fixture prevents creation of Flask test client in fixtures
**Describe the bug**
When the fs fixture is used as input into a fixture yielding a Flask test client, using the test client fixture causes the test to error with the error message "importlib.metadata.PackageNotFoundError: No package metadata was found for werkzeug"

Test stack trace:
```
_______________________________ ERROR at setup of test_app ________________________________

cls = <class '_pytest.runner.CallInfo'>
func = <function call_and_report.<locals>.<lambda> at 0x10bdac820>, when = 'setup'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: Callable[[], TResult],
        when: Literal["collect", "setup", "call", "teardown"],
        reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None,
    ) -> CallInfo[TResult]:
        """Call func, wrapping the result in a CallInfo.
    
        :param func:
            The function to call. Called without arguments.
        :type func: Callable[[], _pytest.runner.TResult]
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: TResult | None = func()

/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/runner.py:341: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/runner.py:242: in <lambda>
    lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/pluggy/_hooks.py:513: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/unraisableexception.py:90: in pytest_runtest_setup
    yield from unraisable_exception_runtest_hook()
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/unraisableexception.py:70: in unraisable_exception_runtest_hook
    yield
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/logging.py:840: in pytest_runtest_setup
    yield from self._runtest_for(item, "setup")
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/logging.py:829: in _runtest_for
    yield
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/capture.py:893: in pytest_runtest_setup
    return (yield)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/threadexception.py:87: in pytest_runtest_setup
    yield from thread_exception_runtest_hook()
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/threadexception.py:68: in thread_exception_runtest_hook
    yield
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/runner.py:160: in pytest_runtest_setup
    item.session._setupstate.setup(item)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/runner.py:514: in setup
    col.setup()
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/python.py:1630: in setup
    self._request._fillfixtures()
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/fixtures.py:697: in _fillfixtures
    item.funcargs[argname] = self.getfixturevalue(argname)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/fixtures.py:532: in getfixturevalue
    fixturedef = self._get_active_fixturedef(argname)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/fixtures.py:617: in _get_active_fixturedef
    fixturedef.execute(request=subrequest)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/fixtures.py:1091: in execute
    result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/pluggy/_hooks.py:513: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/setuponly.py:36: in pytest_fixture_setup
    return (yield)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/fixtures.py:1140: in pytest_fixture_setup
    result = call_fixture_func(fixturefunc, request, kwargs)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/_pytest/fixtures.py:891: in call_fixture_func
    fixture_result = next(generator)
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/conftest.py:16: in test_client
    yield app_fixture.test_client()
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/flask/app.py:723: in test_client
    return cls(  # type: ignore
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/flask/testing.py:131: in __init__
    "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}",
/Users/ryanoldford/Programming/Work/Berlinguette/TryingThingsOut/fakefs_importlib_metadata_bug/.venv/lib/python3.10/site-packages/flask/testing.py:103: in _get_werkzeug_version
    _werkzeug_version = importlib.metadata.version("werkzeug")
/Users/ryanoldford/.pyenv/versions/3.10.5/lib/python3.10/importlib/metadata/__init__.py:984: in version
    return distribution(distribution_name).version
/Users/ryanoldford/.pyenv/versions/3.10.5/lib/python3.10/importlib/metadata/__init__.py:957: in distribution
    return Distribution.from_name(distribution_name)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

cls = <class 'importlib.metadata.Distribution'>, name = 'werkzeug'

    @classmethod
    def from_name(cls, name):
        """Return the Distribution for the given package name.
    
        :param name: The name of the distribution package to search for.
        :return: The Distribution instance (or subclass thereof) for the named
            package, if found.
        :raises PackageNotFoundError: When the named package's distribution
            metadata cannot be found.
        """
        for resolver in cls._discover_resolvers():
            dists = resolver(DistributionFinder.Context(name=name))
            dist = next(iter(dists), None)
            if dist is not None:
                return dist
        else:
>           raise PackageNotFoundError(name)
E           importlib.metadata.PackageNotFoundError: No package metadata was found for werkzeug

/Users/ryanoldford/.pyenv/versions/3.10.5/lib/python3.10/importlib/metadata/__init__.py:548: PackageNotFoundError
```

**How To Reproduce**
Import these packages: Flask 3.1.0, pyfakefs 5.8.0, pytest 8.3.5

Create the following files:
app.py
```
from contextlib import contextmanager
from typing import Generator
from flask import Flask
from local_types import ApiResponse


@contextmanager
def create_app() -> Generator[Flask, None, None]:
    app = Flask(__name__)
    
    @app.route("/", methods=["GET"])
    def basic_get() -> ApiResponse:
        return {"message": "Works!"}, 200
    
    yield app
```

local_types.py
```
from typing import Any, Generator, TypeVar

ApiResponse = tuple[dict[str, Any], int]


T = TypeVar("T")
YieldFixture = Generator[T, Any, Any]
```

conftest.py
```
import pytest
from local_types import YieldFixture
from flask import Flask
from flask.testing import FlaskClient
from app import create_app


@pytest.fixture(scope="function")
def app_fixture(fs) -> YieldFixture[Flask]:
    # mocking goes here
    with create_app() as _app:
        yield _app

@pytest.fixture(scope="function")
def test_client(app_fixture: Flask) -> YieldFixture[FlaskClient]:
    yield app_fixture.test_client()
```

test_app.py
```
from flask.testing import FlaskClient

def test_app(test_client: FlaskClient, fs):
    response = test_client.get("/")
    assert response.status_code == 200
    response_json = response.json
    assert isinstance(response_json, dict)
    assert response_json.get("message") == "Works!"
```

Run pytest and you will see the error. If you remove fs from the fixture parameters, the tests will pass normally.

**Your environment**
macOS-15.3.2-x86_64-i386-64bit
Python 3.10.5 (main, Apr  1 2025, 16:28:06) [Clang 16.0.0 (clang-1600.0.26.6)]
pyfakefs 5.8.0
pytest 8.3.5

Hints from issue comments:
@ROldford: As a workaround, I patched the flask.testing module in the app fixture function as follows:

```
@pytest.fixture(scope="function")
def app_fixture(mocker, fs) -> YieldFixture[Flask]:
    # mocking goes here
    mocker.patch("flask.testing._werkzeug_version", "3.1.3")
    with create_app() as _app:
        yield _app
```

It looks like _werkzeug_version is used only by the _get_werkzeug_version() function in this Flask version, so this shouldn't affect any other code, but I'm still a bit wary of doing this.@mrbean-bremen: Thanks, I will have a look!@mrbean-bremen: Ok, the problem is that `importlib.metadata` accesses the metadata in the filesystem to get the version of Werkzeug. This does not work with pyfakefs, because the metadata does not exist in the fake filesystem.

This kind of problems can usually be fixed by mapping the relevant files into the fake filesystem, though in this case it is a bit ugly:
```py
import importlib
...
from pyfakefs.fake_filesystem_unittest import Pause

@pytest.fixture(scope="function")
def app_fixture(fs) -> YieldFixture[Flask]:
    with Pause(fs):
        # need to pause the patching here as this accesses the real fs
        metadata = [f for f in importlib.metadata.distribution("Werkzeug").files if "METADATA" in str(f)][0]
        metadata_path = metadata.locate()
    fs.add_real_file(metadata_path)
    with create_app() as _app:
        yield _app
```
Maybe a convenience function to map the metadata of a package into the fake fs would make sense. At least there shall be a recipe for resolving this in the troubleshooting section.


Solution patch:
diff --git a/CHANGES.md b/CHANGES.md
index f3697db..956bf4f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -14,6 +14,10 @@ The released versions correspond to PyPI releases.
 
 ## Unreleased
 
+### Enhancements
+* added convenience function `add_package_metadata` to add the metadata of a given
+  package to the fake filesystem (see [#1155](../../issues/1155))
+
 ### Fixes
 * fixed handling of dynamic imports from code in the fake filesystem in Python > 3.11
   (see [#1121](../../issues/1121))
diff --git a/docs/convenience.rst b/docs/convenience.rst
index 5c1424d..3514ad7 100644
--- a/docs/convenience.rst
+++ b/docs/convenience.rst
@@ -114,7 +114,7 @@ You can do the same using ``pytest`` by using a fixture for test setup:
 
 .. note::
   If you are not using the fixture directly in the test, you can use
-  ``@pytest.mark.usefixtures`` instead of passing the fixture as an argument.
+  `@pytest.mark.usefixtures`_ instead of passing the fixture as an argument.
   This avoids warnings about unused arguments from linters.
 
 When using ``pytest`` another option is to load the contents of the real file
@@ -139,6 +139,27 @@ the ``fs`` fixture.
         fs.create_file("fake/path.txt")
         assert content != ""
 
+.. _map-metadata:
+
+Map package metadata files into fake filesystem
+...............................................
+A more specialized function for adding real files to the fake filesystem is
+:py:meth:`add_package_metadata() <pyfakefs.fake_filesystem.FakeFilesystem.add_package_metadata>`.
+It adds the metadata distribution files for a given package to the fake filesystem,
+so that it can be accessed by modules like `importlib.metadata`_. This is needed
+for example if using `flask.testing`_ with ``pyfakefs``.
+
+.. code:: python
+
+    import pytest
+
+
+    @pytest.fixture(autouse=True)
+    def add_werkzeug_metadata(fs):
+        # flask.testing accesses Werkzeug metadata, map it
+        fs.add_package_metadata("Werkzeug")
+        yield
+
 
 Handling mount points
 ~~~~~~~~~~~~~~~~~~~~~
@@ -312,3 +333,8 @@ possibility to use the ``force_unix_mode`` argument to ``FakeFilesystem.chmod``:
         fs.chmod("/foo", 0o000, force_unix_mode=True)
         with pytest.raises(PermissionError):
             path.is_file()
+
+
+.. _`importlib.metadata`: https://docs.python.org/3/library/importlib.metadata.html
+.. _`@pytest.mark.usefixtures`: https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-usefixtures
+.. _`flask.testing`: https://flask-testing.readthedocs.io/en/latest/
diff --git a/docs/modules.rst b/docs/modules.rst
index 55dae71..f978e36 100644
--- a/docs/modules.rst
+++ b/docs/modules.rst
@@ -14,7 +14,7 @@ Fake filesystem classes
 .. autoclass:: pyfakefs.fake_filesystem.FakeFilesystem
     :members: add_mount_point,
         get_disk_usage, set_disk_usage, change_disk_usage,
-        add_real_directory, add_real_file, add_real_symlink, add_real_paths,
+        add_real_directory, add_real_file, add_real_symlink, add_real_paths, add_package_metadata,
         create_dir, create_file, create_symlink, create_link,
         get_object, pause, resume
 
diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst
index 3f3697c..8ac8b9e 100644
--- a/docs/troubleshooting.rst
+++ b/docs/troubleshooting.rst
@@ -182,6 +182,10 @@ Here's an example of how to add these using pytest:
         )
         return fs
 
+Another example of code accessing the real filesystem is the usage of `importlib.metadata`_,
+which accesses the metadata associated with a Python package on disk. To handle this
+specific problem, another convenience method can be used - see :ref:`map-metadata`.
+
 .. _os_temporary_directories:
 
 OS temporary directories
@@ -491,3 +495,4 @@ We will analyze the problem, and if we find a solution we will either get this f
 .. _`pandas`: https://pypi.org/project/pandas/
 .. _`xlrd`: https://pypi.org/project/xlrd/
 .. _`openpyxl`: https://pypi.org/project/openpyxl/
+.. _`importlib.metadata`: https://docs.python.org/3/library/importlib.metadata.html
diff --git a/pyfakefs/fake_file.py b/pyfakefs/fake_file.py
index 73aec9d..f56ac9a 100644
--- a/pyfakefs/fake_file.py
+++ b/pyfakefs/fake_file.py
@@ -236,7 +236,7 @@ class FakeFile:
         self.stat_result.st_mtime = val
 
     def set_large_file_size(self, st_size: int) -> None:
-        """Sets the self.st_size attribute and replaces self.content with None.
+        """Sets the self.st_size attribute and replaces self.content with `None`.
 
         Provided specifically to simulate very large files without regards
         to their content (which wouldn't fit in memory).
@@ -247,8 +247,8 @@ class FakeFile:
           st_size: (int) The desired file size
 
         Raises:
-          OSError: if the st_size is not a non-negative integer,
-                   or if st_size exceeds the available file system space
+          OSError: if ``st_size`` is not a non-negative integer,
+                   or if ``st_size`` exceeds the available file system space
         """
         self._check_positive_int(st_size)
         if self.st_size:
@@ -286,11 +286,11 @@ class FakeFile:
             contents: string, new content of file.
 
         Returns:
-            True if the contents have been changed.
+            `True` if the contents have been changed.
 
         Raises:
-              OSError: if the st_size is not a non-negative integer,
-                   or if st_size exceeds the available file system space
+              OSError: if the `st_size` is not a non-negative integer,
+                   or if `st_size` exceeds the available file system space
         """
         byte_contents = self._encode_contents(contents)
         changed = self._byte_contents != byte_contents
@@ -312,11 +312,11 @@ class FakeFile:
         Args:
           contents: (str, bytes) new content of file.
           encoding: (str) the encoding to be used for writing the contents
-                    if they are a unicode string.
+                    if they are a Unicode string.
                     If not given, the locale preferred encoding is used.
 
         Returns:
-            True if the contents have been changed.
+            `True` if the contents have been changed.
 
         Raises:
           OSError: if `st_size` is not a non-negative integer,
@@ -408,7 +408,7 @@ class FakeFile:
             permission_bits: The permission bits as set for the user.
 
         Returns:
-            True if the permissions are set in the correct class (user/group/other).
+            `True` if the permissions are set in the correct class (user/group/other).
         """
         if helpers.get_uid() == self.stat_result.st_uid:
             return self.st_mode & permission_bits == permission_bits
@@ -579,7 +579,7 @@ class FakeDirectory(FakeFile):
 
         Args:
             pathname_name: Basename of the child object to remove.
-            recursive: If True (default), the entries in contained directories
+            recursive: If `True` (default), the entries in contained directories
                 are deleted first. Used to propagate removal errors
                 (e.g. permission problems) from contained entries.
 
diff --git a/pyfakefs/fake_filesystem.py b/pyfakefs/fake_filesystem.py
index 08f45dd..7df9f17 100644
--- a/pyfakefs/fake_filesystem.py
+++ b/pyfakefs/fake_filesystem.py
@@ -92,6 +92,7 @@ import tempfile
 from collections import namedtuple, OrderedDict
 from doctest import TestResults
 from enum import Enum
+
 from stat import (
     S_IFREG,
     S_IFDIR,
@@ -190,8 +191,9 @@ class FakeFilesystem:
         umask: The umask used for newly created files, see `os.umask`.
         patcher: Holds the Patcher object if created from it. Allows access
             to the patcher object if using the pytest fs fixture.
-        patch_open_code: Defines how `io.open_code` will be patched;
-            patching can be on, off, or in automatic mode.
+        patch_open_code: Defines how
+            `io.open_code <https://docs.python.org/3/library/io.html#io.open_code>`__
+            will be patched; patching can be on, off, or in automatic mode.
         shuffle_listdir_results: If `True`, `os.listdir` will not sort the
             results to match the real file system behavior.
     """
@@ -206,10 +208,10 @@ class FakeFilesystem:
         """
         Args:
             path_separator:  optional substitute for os.path.sep
-            total_size: if not None, the total size in bytes of the
+            total_size: if not `None`, the total size in bytes of the
                 root filesystem.
             patcher: the Patcher instance if created from the Patcher
-            create_temp_dir: If True, a temp directory is created on initialization.
+            create_temp_dir: If `True`, a temp directory is created on initialization.
                 Under Posix, if the temp directory is not `/tmp`, a link to the temp
                 path is additionally created at `/tmp`.
 
@@ -500,7 +502,7 @@ class FakeFilesystem:
         return matching_string(path, self.alternative_path_separator)
 
     def starts_with_sep(self, path: AnyStr) -> bool:
-        """Return True if path starts with a path separator."""
+        """Return `True` if path starts with a path separator."""
         sep = self.get_path_separator(path)
         altsep = self._alternative_path_separator(path)
         return path.startswith(sep) or altsep is not None and path.startswith(altsep)
@@ -528,7 +530,7 @@ class FakeFilesystem:
 
         Raises:
             OSError: if trying to mount an existing mount point again,
-                and `can_exist` is False.
+                and ``can_exist`` is `False`.
         """
         path = self.normpath(self.normcase(path))
         for mount_point in self.mount_points:
@@ -717,15 +719,15 @@ class FakeFilesystem:
             mount_point["used_size"] += usage_change
 
     def stat(self, entry_path: AnyStr, follow_symlinks: bool = True):
-        """Return the os.stat-like tuple for the FakeFile object of entry_path.
+        """Return the os.stat-like tuple for the FakeFile object of ``entry_path``.
 
         Args:
             entry_path:  Path to filesystem object to retrieve.
-            follow_symlinks: If False and entry_path points to a symlink,
+            follow_symlinks: If `False` and ``entry_path`` points to a symlink,
                 the link itself is inspected instead of the linked object.
 
         Returns:
-            The FakeStatResult object corresponding to entry_path.
+            The `FakeStatResult` object corresponding to ``entry_path``.
 
         Raises:
             OSError: if the filesystem object doesn't exist.
@@ -796,7 +798,7 @@ class FakeFilesystem:
             mode: (int) Permissions.
             follow_symlinks: If `False` and `path` points to a symlink,
                 the link itself is affected instead of the linked object.
-            force_unix_mode: if True and run under Windows, the mode is not
+            force_unix_mode: if `True` and run under Windows, the mode is not
                 adapted for Windows to allow making dirs unreadable
         """
         allow_fd = not self.is_windows_fs or sys.version_info >= (3, 13)
@@ -828,18 +830,18 @@ class FakeFilesystem:
             path: (str) Path to the file.
             times: 2-tuple of int or float numbers, of the form (atime, mtime)
                 which is used to set the access and modified times in seconds.
-                If None, both times are set to the current time.
+                If `None`, both times are set to the current time.
             ns: 2-tuple of int numbers, of the form (atime, mtime)  which is
                 used to set the access and modified times in nanoseconds.
                 If `None`, both times are set to the current time.
-            follow_symlinks: If `False` and entry_path points to a symlink,
+            follow_symlinks: If `False` and ``path`` points to a symlink,
                 the link itself is queried instead of the linked object.
 
             Raises:
                 TypeError: If anything other than the expected types is
-                    specified in the passed `times` or `ns` tuple,
+                    specified in the passed ``times`` or ``ns`` tuple,
                     or if the tuple length is not equal to 2.
-                ValueError: If both times and ns are specified.
+                ValueError: If both ``times`` and ``ns`` are specified.
         """
         self._handle_utime_arg_errors(ns, times)
 
@@ -922,7 +924,7 @@ class FakeFilesystem:
         """Remove file object with given descriptor from the list
         of open files.
 
-        Sets the entry in open_files to None.
+        Sets the entry in `open_files` to `None`.
 
         Args:
             file_des: Descriptor of file object to be removed from
@@ -970,7 +972,7 @@ class FakeFilesystem:
         self.raise_os_error(errno.EBADF, str(file_des))
 
     def has_open_file(self, file_object: FakeFile) -> bool:
-        """Return True if the given file object is in the list of open files.
+        """Return `True` if the given file object is in the list of open files.
 
         Args:
             file_object: The FakeFile object to be checked.
@@ -1371,7 +1373,7 @@ class FakeFilesystem:
         return path_components
 
     def starts_with_drive_letter(self, file_path: AnyStr) -> bool:
-        """Return True if file_path starts with a drive letter.
+        """Return `True` if file_path starts with a drive letter.
 
         Args:
             file_path: the full path to be examined.
@@ -1457,7 +1459,7 @@ class FakeFilesystem:
         return False
 
     def ends_with_path_separator(self, path: Union[int, AnyPath]) -> bool:
-        """Return True if ``file_path`` ends with a valid path separator."""
+        """Return `True` if ``file_path`` ends with a valid path separator."""
         if isinstance(path, int):
             return False
         file_path = make_string_path(path)
@@ -1497,13 +1499,13 @@ class FakeFilesystem:
 
         Args:
             file_path:  The path to examine.
-            check_link: If True, links are not followed
+            check_link: If `True`, links are not followed
 
         Returns:
-            (bool) True if the corresponding object exists.
+            (bool) `True` if the corresponding object exists.
 
         Raises:
-            TypeError: if file_path is None.
+            TypeError: if file_path is `None`.
         """
         if check_link and self.islink(file_path):
             return True
@@ -1718,11 +1720,11 @@ class FakeFilesystem:
         Args:
             file_path: Specifies target FakeFile object to retrieve, with a
                 path that has already been normalized/resolved.
-            check_read_perm: If True, raises OSError if a parent directory
+            check_read_perm: If `True`, raises `OSError` if a parent directory
                 does not have read permission
-            check_exe_perm: If True, raises OSError if a parent directory
+            check_exe_perm: If `True`, raises `OSError` if a parent directory
                 does not have execute (e.g. search) permission
-            check_owner: If True, and check_read_perm is also True,
+            check_owner: If `True`, and ``check_read_perm`` is also `True`,
                 only checks read permission if the current user id is
                 different from the file object user id
 
@@ -1787,9 +1789,9 @@ class FakeFilesystem:
         Args:
             file_path: Specifies the target
                 :py:class:`FakeFile<pyfakefs.fake_file.FakeFile>` object to retrieve.
-            check_read_perm: If True, raises OSError if a parent directory
+            check_read_perm: If `True`, raises `OSError` if a parent directory
                 does not have read permission
-            check_exe_perm: If True, raises OSError if a parent directory
+            check_exe_perm: If `True`, raises `OSError` if a parent directory
                 does not have execute (e.g. search) permission
 
         Returns:
@@ -1836,16 +1838,16 @@ class FakeFilesystem:
             follow_symlinks: If `False`, the link itself is resolved,
                 otherwise the object linked to.
             allow_fd: If `True`, `file_path` may be an open file descriptor
-            check_read_perm: If True, raises OSError if a parent directory
+            check_read_perm: If `True`, raises `OSError` if a parent directory
                 does not have read permission
-            check_read_perm: If True, raises OSError if a parent directory
+            check_read_perm: If `True`, raises `OSError` if a parent directory
                 does not have execute permission
-            check_owner: If True, and check_read_perm is also True,
+            check_owner: If `True`, and ``check_read_perm`` is also `True`,
                 only checks read permission if the current user id is
                 different from the file object user id
 
         Returns:
-          The FakeFile object corresponding to `file_path`.
+          The FakeFile object corresponding to ``file_path``.
 
         Raises:
             OSError: if the object is not found.
@@ -2279,7 +2281,7 @@ class FakeFilesystem:
             create_missing_dirs: If `True`, auto create missing directories.
             apply_umask: If `True` (default), the current umask is applied
                 to `st_mode`.
-            encoding: If `contents` is of type `str`, the encoding used
+            encoding: If ``contents`` is of type `str`, the encoding used
                 for serialization.
             errors: The error mode used for encoding/decoding errors.
             side_effect: function handle that is executed when the file is written,
@@ -2525,6 +2527,30 @@ class FakeFilesystem:
             else:
                 self.add_real_file(path, read_only)
 
+    def add_package_metadata(self, package_name: str) -> None:
+        """Convenience function to add all metadata distribution files of the package
+        with the given name to the fake filesystem. These are the files that are
+        usually located under `<package_name>-<version>.dist-info` beside the actual
+        package. The files are accessed by the functions in `importlib.metadata`.
+
+        Args:
+            package_name: Name of the package whose metadata shall be copied
+
+        Raises:
+            PackageNotFoundError: if the package with the given name is not found
+        """
+        if sys.version_info < (3, 8):
+            raise NotImplementedError("Not available in Python 3.7")
+
+        from importlib.metadata import distribution, PackageNotFoundError
+
+        dist_files = distribution(package_name).files
+        if dist_files is None:
+            raise PackageNotFoundError(package_name)
+
+        for metadata_file in dist_files:
+            self.add_real_file(metadata_file.locate())
+
     def create_file_internally(
         self,
         file_path: AnyPath,
@@ -2549,13 +2575,13 @@ class FakeFilesystem:
             st_size: file size; only valid if contents not given. If given,
                 the file is considered to be in "large file mode" and trying
                 to read from or write to the file will result in an exception.
-            create_missing_dirs: if True, auto create missing directories.
-            apply_umask: whether or not the current umask must be applied
+            create_missing_dirs: if `True`, auto create missing directories.
+            apply_umask: whether the current umask must be applied
                 on st_mode.
-            encoding: if contents is a unicode string, the encoding used for
+            encoding: if contents is a Unicode string, the encoding used for
                 serialization.
             errors: the error mode used for encoding/decoding errors
-            read_from_real_fs: if True, the contents are read from the real
+            read_from_real_fs: if `True`, the contents are read from the real
                 file system on demand.
             side_effect: function handle that is executed when file is written,
                 must accept the file object as an argument.
@@ -2756,15 +2782,15 @@ class FakeFilesystem:
         Args:
             old_path: An existing link to the target file.
             new_path: The destination path to create a new link at.
-            follow_symlinks: If False and old_path is a symlink, link the
+            follow_symlinks: If `False` and ``old_path`` is a symlink, link the
                 symlink instead of the object it points to.
 
         Returns:
-            The FakeFile object referred to by old_path.
+            The FakeFile object referred to by ``old_path``.
 
         Raises:
             OSError:  if something already exists at new_path.
-            OSError:  if old_path is a directory.
+            OSError:  if ``old_path`` is a directory.
             OSError:  if the parent directory doesn't exist.
         """
         return self.create_link(
@@ -2789,7 +2815,7 @@ class FakeFilesystem:
             the string representing the path to which the symbolic link points.
 
         Raises:
-            TypeError: if path is None
+            TypeError: if path is `None`
             OSError: (with errno=ENOENT) if path is not a valid path, or
                 (with errno=EINVAL) if path is valid, but is not a symlink,
                 or if the path ends with a path separator (Posix only)
@@ -2886,7 +2912,7 @@ class FakeFilesystem:
             mode: (int) Mode to create directory (and any necessary parent
                 directories) with. This argument defaults to 0o777.
                 The umask is applied to this mode.
-          exist_ok: (boolean) If exist_ok is False (the default), an OSError is
+          exist_ok: (boolean) If exist_ok is `False` (the default), an `OSError` is
                 raised if the target directory already exists.
 
         Raises:
@@ -2952,7 +2978,7 @@ class FakeFilesystem:
             (boolean) `True` if the st_flag is set in path's st_mode.
 
         Raises:
-          TypeError: if path is None
+          TypeError: if path is `None`
         """
         if path is None:
             raise TypeError
@@ -2980,7 +3006,7 @@ class FakeFilesystem:
             `True` if path points to a directory (following symlinks).
 
         Raises:
-            TypeError: if path is None.
+            TypeError: if path is `None`.
         """
         return self._is_of_type(path, S_IFDIR, follow_symlinks)
 
@@ -2996,7 +3022,7 @@ class FakeFilesystem:
             `True` if path points to a regular file (following symlinks).
 
         Raises:
-            TypeError: if path is None.
+            TypeError: if path is `None`.
         """
         return self._is_of_type(path, S_IFREG, follow_symlinks)
 
@@ -3010,14 +3036,14 @@ class FakeFilesystem:
             `True` if path points to a symlink (S_IFLNK set in st_mode)
 
         Raises:
-            TypeError: if path is None.
+            TypeError: if path is `None`.
         """
         return self._is_of_type(path, S_IFLNK, follow_symlinks=False)
 
     if sys.version_info >= (3, 12):
 
         def isjunction(self, path: AnyPath) -> bool:
-            """Returns False. Junctions are never faked."""
+            """Returns `False`. Junctions are never faked."""
             return False
 
     def confirmdir(
@@ -3033,11 +3059,11 @@ class FakeFilesystem:
         Args:
             target_directory: Path to the target directory within the fake
                 filesystem.
-            check_read_perm: If True, raises OSError if the directory
+            check_read_perm: If `True`, raises `OSError` if the directory
                 does not have read permission
-            check_exe_perm: If True, raises OSError if the directory
+            check_exe_perm: If `True`, raises `OSError` if the directory
                 does not have execute (e.g. search) permission
-            check_owner: If True, only checks read permission if the current
+            check_owner: If `True`, only checks read permission if the current
                 user id is different from the file object user id
 
         Returns:
diff --git a/pyfakefs/fake_filesystem_unittest.py b/pyfakefs/fake_filesystem_unittest.py
index 7f8a20b..273f425 100644
--- a/pyfakefs/fake_filesystem_unittest.py
+++ b/pyfakefs/fake_filesystem_unittest.py
@@ -364,7 +364,7 @@ class TestCaseMixin:
         Invoke this at the beginning of the `setUp()` method in your unit test
         class.
         For the arguments, see the `TestCaseMixin` attribute description.
-        If any of the arguments is not None, it overwrites the settings for
+        If any of the arguments is not `None`, it overwrites the settings for
         the current test case. Settings the arguments here may be a more
         convenient way to adapt the setting than overwriting `__init__()`.
         """
@@ -632,18 +632,18 @@ class Patcher:
             modules_to_patch: A dictionary of fake modules mapped to the
                 fully qualified patched module names. Can be used to add
                 patching of modules not provided by `pyfakefs`.
-            allow_root_user: If True (default), if the test is run as root
+            allow_root_user: If `True` (default), if the test is run as root
                 user, the user in the fake file system is also considered a
                 root user, otherwise it is always considered a regular user.
-            use_known_patches: If True (the default), some patches for commonly
+            use_known_patches: If `True` (the default), some patches for commonly
                 used packages are applied which make them usable with pyfakefs.
-            patch_open_code: If True, `io.open_code` is patched. The default
+            patch_open_code: If `True`, `io.open_code` is patched. The default
                 is not to patch it, as it mostly is used to load compiled
                 modules that are not in the fake file system.
-            patch_default_args: If True, default arguments are checked for
+            patch_default_args: If `True`, default arguments are checked for
                 file system functions, which are patched. This check is
                 expansive, so it is off by default.
-            use_cache: If True (default), patched and non-patched modules are
+            use_cache: If `True` (default), patched and non-patched modules are
                 cached between tests for performance reasons. As this is a new
                 feature, this argument allows to turn it off in case it
                 causes any problems.
diff --git a/pyfakefs/fake_io.py b/pyfakefs/fake_io.py
index ed9194c..2a8cb38 100644
--- a/pyfakefs/fake_io.py
+++ b/pyfakefs/fake_io.py
@@ -52,9 +52,10 @@ class PatchMode(Enum):
 class FakeIoModule:
     """Uses FakeFilesystem to provide a fake io module replacement.
 
-    You need a fake_filesystem to use this:
-    filesystem = fake_filesystem.FakeFilesystem()
-    my_io_module = fake_io.FakeIoModule(filesystem)
+    You need a fake_filesystem to use this::
+
+        filesystem = fake_filesystem.FakeFilesystem()
+        my_io_module = fake_io.FakeIoModule(filesystem)
     """
 
     @staticmethod
diff --git a/pyfakefs/fake_open.py b/pyfakefs/fake_open.py
index be35c02..d3dcd4e 100644
--- a/pyfakefs/fake_open.py
+++ b/pyfakefs/fake_open.py
@@ -113,8 +113,8 @@ def fake_open(
 class FakeFileOpen:
     """Faked `file()` and `open()` function replacements.
 
-    Returns FakeFile objects in a FakeFilesystem in place of the `file()`
-    or `open()` function.
+    Returns :py:class:`FakeDirectory<pyfakefs.fake_file.FakeFile>` objects in a
+        fake filesystem; replaces the `open()` function.
     """
 
     __name__ = "FakeFileOpen"
diff --git a/pyfakefs/fake_os.py b/pyfakefs/fake_os.py
index b446026..e786484 100644
--- a/pyfakefs/fake_os.py
+++ b/pyfakefs/fake_os.py
@@ -79,14 +79,15 @@ NR_STD_STREAMS = 3
 class FakeOsModule:
     """Uses FakeFilesystem to provide a fake os module replacement.
 
-    Do not create os.path separately from os, as there is a necessary circular
-    dependency between os and os.path to replicate the behavior of the standard
-    Python modules.  What you want to do is to just let FakeOsModule take care
+    Do not create `os.path` separately from `os`, as there is a necessary circular
+    dependency between `os` and `os.path` to replicate the behavior of the standard
+    Python modules.  What you want to do is to just let `FakeOsModule` take care
     of `os.path` setup itself.
 
-    # You always want to do this.
-    filesystem = fake_filesystem.FakeFilesystem()
-    my_os_module = fake_os.FakeOsModule(filesystem)
+    If you are creating the fake filesystem manually, you need to do::
+
+        filesystem = fake_filesystem.FakeFilesystem()
+        my_os_module = fake_os.FakeOsModule(filesystem)
     """
 
     use_original = False
@@ -510,11 +511,11 @@ class FakeOsModule:
         Args:
             path: File path, file descriptor or path-like object.
             attribute: (str or bytes) The attribute name.
-            follow_symlinks: (bool) If True (the default), symlinks in the
+            follow_symlinks: (bool) If `True` (the default), symlinks in the
                 path are traversed.
 
         Returns:
-            The contents of the extended attribute as bytes or None if
+            The contents of the extended attribute as bytes or `None` if
             the attribute does not exist.
 
         Raises:
@@ -537,8 +538,8 @@ class FakeOsModule:
 
         Args:
             path: File path, file descriptor or path-like object.
-               If None, the current directory is used.
-            follow_symlinks: (bool) If True (the default), symlinks in the
+               If `None`, the current directory is used.
+            follow_symlinks: (bool) If `True` (the default), symlinks in the
                 path are traversed.
 
         Returns:
@@ -566,7 +567,7 @@ class FakeOsModule:
         Args:
             path: File path, file descriptor or path-like object
             attribute: (str or bytes) The attribute name.
-            follow_symlinks: (bool) If True (the default), symlinks in the
+            follow_symlinks: (bool) If `True` (the default), symlinks in the
                 path are traversed.
 
         Raises:
@@ -597,7 +598,7 @@ class FakeOsModule:
             path: File path, file descriptor or path-like object.
             attribute: The attribute name (str or bytes).
             value: (byte-like) The value to be set.
-            follow_symlinks: (bool) If True (the default), symlinks in the
+            follow_symlinks: (bool) If `True` (the default), symlinks in the
                 path are traversed.
 
         Raises:
@@ -672,7 +673,7 @@ class FakeOsModule:
             the string representing the path to which the symbolic link points.
 
         Raises:
-            TypeError: if `path` is None
+            TypeError: if `path` is `None`
             OSError: (with errno=ENOENT) if path is not a valid path, or
                      (with errno=EINVAL) if path is valid, but is not a symlink
         """
@@ -933,7 +934,7 @@ class FakeOsModule:
             mode: (int) Mode to create directory (and any necessary parent
                 directories) with. This argument defaults to 0o777.
                 The umask is applied to this mode.
-            exist_ok: (boolean) If exist_ok is False (the default), an OSError
+            exist_ok: (boolean) If exist_ok is `False` (the default), an `OSError`
                 is raised if the target directory already exists.
 
         Raises:
@@ -1145,10 +1146,10 @@ class FakeOsModule:
             path: (str) Path to the file.
             times: 2-tuple of int or float numbers, of the form (atime, mtime)
                 which is used to set the access and modified times in seconds.
-                If None, both times are set to the current time.
+                If `None`, both times are set to the current time.
             ns: 2-tuple of int numbers, of the form (atime, mtime)  which is
                 used to set the access and modified times in nanoseconds.
-                If None, both times are set to the current time.
+                If `None`, both times are set to the current time.
             dir_fd: If not `None`, the file descriptor of a directory,
                 with `path` being relative to this directory.
             follow_symlinks: (bool) If `False` and `path` points to a symlink,
@@ -1156,9 +1157,9 @@ class FakeOsModule:
 
             Raises:
                 TypeError: If anything other than the expected types is
-                    specified in the passed `times` or `ns` tuple,
+                    specified in the passed ``times`` or ``ns`` tuple,
                     or if the tuple length is not equal to 2.
-                ValueError: If both times and ns are specified.
+                ValueError: If both ``times`` and ``ns`` are specified.
         """
         path = self._path_with_dir_fd(path, self.utime, dir_fd)
         self.filesystem.utime(path, times=times, ns=ns, follow_symlinks=follow_symlinks)
@@ -1291,7 +1292,7 @@ class FakeOsModule:
                 with `src` being relative to this directory.
             dst_dir_fd: If not `None`, the file descriptor of a directory,
                 with `dst` being relative to this directory.
-            follow_symlinks: (bool) If True (the default), symlinks in the
+            follow_symlinks: (bool) If `True` (the default), symlinks in the
                 path are traversed.
 
         Raises:
diff --git a/pyfakefs/fake_path.py b/pyfakefs/fake_path.py
index d421b11..2c6f6b4 100644
--- a/pyfakefs/fake_path.py
+++ b/pyfakefs/fake_path.py
@@ -63,8 +63,8 @@ def _copy_module(old: ModuleType) -> ModuleType:
 class FakePathModule:
     """Faked os.path module replacement.
 
-    FakePathModule should *only* be instantiated by FakeOsModule.  See the
-    FakeOsModule docstring for details.
+    `FakePathModule` should *only* be instantiated by `FakeOsModule`.  See the
+    :py:class:`FakeDirectory<pyfakefs.fake_os.FakeOsModule>` docstring for details.
     """
 
     _OS_PATH_COPY: Any = _copy_module(os.path)
@@ -139,7 +139,7 @@ class FakePathModule:
         return self.filesystem.exists(path)
 
     def lexists(self, path: AnyStr) -> bool:
-        """Test whether a path exists.  Returns True for broken symbolic links.
+        """Test whether a path exists.  Returns `True` for broken symbolic links.
 
         Args:
           path:  path to the symlink object.
@@ -168,7 +168,7 @@ class FakePathModule:
         return file_obj.st_size
 
     def isabs(self, path: AnyStr) -> bool:
-        """Return True if path is an absolute pathname."""
+        """Return `True` if path is an absolute pathname."""
         empty = matching_string(path, "")
         if self.filesystem.is_windows_fs:
             drive, path = self.splitdrive(path)
@@ -201,14 +201,14 @@ class FakePathModule:
             `True` if path points to a symbolic link.
 
         Raises:
-            TypeError: if path is None.
+            TypeError: if path is `None`.
         """
         return self.filesystem.islink(path)
 
     if sys.version_info >= (3, 12):
 
         def isjunction(self, path: AnyStr) -> bool:
-            """Returns False. Junctions are never faked."""
+            """Returns `False`. Junctions are never faked."""
             return self.filesystem.isjunction(path)
 
         def splitroot(self, path: AnyStr):
@@ -473,7 +473,7 @@ class FakePathModule:
 
         Returns:
             `True` if path is a mount point added to the fake file system.
-            Under Windows also returns True for drive and UNC roots
+            Under Windows also returns `True` for drive and UNC roots
             (independent of their existence).
         """
         if not path:
diff --git a/pyfakefs/fake_pathlib.py b/pyfakefs/fake_pathlib.py
index 178b2ca..c2d5278 100644
--- a/pyfakefs/fake_pathlib.py
+++ b/pyfakefs/fake_pathlib.py
@@ -446,7 +446,7 @@ if sys.version_info < (3, 12):
         pathmod = ntpath
 
         def is_reserved(self, parts):
-            """Return True if the path is considered reserved under Windows."""
+            """Return `True` if the path is considered reserved under Windows."""
 
             # NOTE: the rules for reserved names seem somewhat complicated
             # (e.g. r"..\NUL" is reserved but not r"foo\NUL").
@@ -662,7 +662,7 @@ class FakePath(pathlib.Path):
             under Windows).
 
             Args:
-                strict: If False (default) no exception is raised if the path
+                strict: If `False` (default) no exception is raised if the path
                     does not exist.
 
             Raises:
@@ -831,11 +831,11 @@ class FakePath(pathlib.Path):
 
         Args:
             mode: the file mode for the file if it does not exist
-            exist_ok: if the file already exists and this is True, nothing
-                happens, otherwise FileExistError is raised
+            exist_ok: if the file already exists and this is `True`, nothing
+                happens, otherwise `FileExistError` is raised
 
         Raises:
-            FileExistsError: if the file exists and exits_ok is False.
+            FileExistsError: if the file exists and ``exits_ok`` is `False`.
         """
         self._raise_on_closed()
         if self.exists():
@@ -862,9 +862,10 @@ def _warn_is_reserved_deprecated():
 class FakePathlibModule:
     """Uses FakeFilesystem to provide a fake pathlib module replacement.
 
-    You need a fake_filesystem to use this:
-    `filesystem = fake_filesystem.FakeFilesystem()`
-    `fake_pathlib_module = fake_pathlib.FakePathlibModule(filesystem)`
+    You need a fake_filesystem to use this::
+
+        filesystem = fake_filesystem.FakeFilesystem()
+        fake_pathlib_module = fake_pathlib.FakePathlibModule(filesystem)
     """
 
     def __init__(self, filesystem):
diff --git a/pyfakefs/fake_scandir.py b/pyfakefs/fake_scandir.py
index ae69601..614dc6c 100644
--- a/pyfakefs/fake_scandir.py
+++ b/pyfakefs/fake_scandir.py
@@ -50,42 +50,42 @@ class DirEntry(os.PathLike):
         return self._inode
 
     def is_dir(self, follow_symlinks=True):
-        """Return True if this entry is a directory entry.
+        """Return `True` if this entry is a directory entry.
 
         Args:
-            follow_symlinks: If True, also return True if this entry is a
+            follow_symlinks: If `True`, also return `True` if this entry is a
                 symlink pointing to a directory.
 
         Returns:
-            True if this entry is an existing directory entry, or if
-                follow_symlinks is set, and this entry points to an existing
+            `True` if this entry is an existing directory entry, or if
+                ``follow_symlinks`` is set, and this entry points to an existing
                 directory entry.
         """
         return self._isdir and (follow_symlinks or not self._islink)
 
     def is_file(self, follow_symlinks=True):
-        """Return True if this entry is a regular file entry.
+        """Return `True` if this entry is a regular file entry.
 
         Args:
-            follow_symlinks: If True, also return True if this entry is a
+            follow_symlinks: If `True`, also return `True` if this entry is a
                 symlink pointing to a regular file.
 
         Returns:
-            True if this entry is an existing file entry, or if
-                follow_symlinks is set, and this entry points to an existing
+            `True` if this entry is an existing file entry, or if
+                ``follow_symlinks`` is set, and this entry points to an existing
                 file entry.
         """
         return not self._isdir and (follow_symlinks or not self._islink)
 
     def is_symlink(self):
-        """Return True if this entry is a symbolic link (even if broken)."""
+        """Return `True` if this entry is a symbolic link (even if broken)."""
         return self._islink
 
     def stat(self, follow_symlinks=True):
-        """Return a stat_result object for this entry.
+        """Return a `stat_result` object for this entry.
 
         Args:
-            follow_symlinks: If False and the entry is a symlink, return the
+            follow_symlinks: If `False` and the entry is a symlink, return the
                 result for the symlink, otherwise for the object it points to.
         """
         if follow_symlinks:
@@ -110,7 +110,7 @@ class DirEntry(os.PathLike):
     if sys.version_info >= (3, 12):
 
         def is_junction(self) -> bool:
-            """Return True if this entry is a junction.
+            """Return `True` if this entry is a junction.
             Junctions are not a part of posix semantic."""
             if not self._filesystem.is_windows_fs:
                 return False
diff --git a/pyfakefs/helpers.py b/pyfakefs/helpers.py
index 0e08813..f4e2f15 100644
--- a/pyfakefs/helpers.py
+++ b/pyfakefs/helpers.py
@@ -10,7 +10,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Helper classes use for fake file system implementation."""
+"""Helper classes used for the fake file system implementation and may be useful
+for ``pyfakefs`` users."""
 
 import ctypes
 import importlib
@@ -110,23 +111,23 @@ def reset_ids() -> None:
 
 
 def is_root() -> bool:
-    """Return True if the current user is the root user."""
+    """Return `True` if the current user is the root user."""
     return USER_ID == 0
 
 
 def is_int_type(val: Any) -> bool:
-    """Return True if `val` is of integer type."""
+    """Return `True` if `val` is of integer type."""
     return isinstance(val, int)
 
 
 def is_byte_string(val: Any) -> bool:
-    """Return True if `val` is a bytes-like object, False for a unicode
+    """Return `True` if ``val`` is a bytes-like object, `False` for a Unicode
     string."""
     return not hasattr(val, "encode")
 
 
 def is_unicode_string(val: Any) -> bool:
-    """Return True if `val` is a unicode string, False for a bytes-like
+    """Return `True` if ``val`` is a Unicode string, `False` for a bytes-like
     object."""
     return hasattr(val, "encode")
 
@@ -172,7 +173,7 @@ def join_strings(s1: AnyStr, s2: AnyStr) -> AnyStr:
 
 def real_encoding(encoding: Optional[str]) -> Optional[str]:
     """Since Python 3.10, the new function ``io.text_encoding`` returns
-    "locale" as the encoding if None is defined. This will be handled
+    "locale" as the encoding if none is defined. This will be handled
     as no encoding in pyfakefs."""
     if sys.version_info >= (3, 10):
         return encoding if encoding != "locale" else None


Test patch:
diff --git a/pyfakefs/tests/fake_filesystem_test.py b/pyfakefs/tests/fake_filesystem_test.py
index d851d05..a245732 100644
--- a/pyfakefs/tests/fake_filesystem_test.py
+++ b/pyfakefs/tests/fake_filesystem_test.py
@@ -25,6 +25,11 @@ import tempfile
 import unittest
 from unittest.mock import patch
 
+try:
+    import pytest
+except ImportError:
+    pytest = None
+
 from pyfakefs import fake_filesystem, fake_os, fake_open
 from pyfakefs.fake_filesystem import (
     set_uid,
@@ -2607,6 +2612,17 @@ class RealFileSystemAccessTest(RealFsTestCase):
         self.check_fake_file_stat(fake_file, real_file_path)
         self.check_writable_file(fake_file, real_file_path)
 
+    @unittest.skipIf(pytest is None, "pytest is not installed")
+    @unittest.skipIf(sys.version_info < (3, 8), "importlib.metadata not available")
+    def test_add_package_metadata(self):
+        parent_path = pathlib.Path(pytest.__file__).parent.parent
+        pytest_dist_path = parent_path / f"pytest-{pytest.__version__}.dist-info"
+        assert pytest_dist_path.exists()  # real fs
+        assert not self.filesystem.exists(pytest_dist_path)
+        self.filesystem.add_package_metadata("pytest")
+        assert self.filesystem.exists(pytest_dist_path)
+        assert self.filesystem.exists(pytest_dist_path / "METADATA")
+
 
 class FileSideEffectTests(TestCase):
     def side_effect(self):