H", tag_data[:2])[0]):
+ ifd_tag, typ, count, data = struct.unpack(
+ ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2]
+ )
+ if ifd_tag == 0x1101:
+ # CameraInfo
+ (offset,) = struct.unpack(">L", data)
+ self.fp.seek(offset)
+
+ camerainfo: dict[str, int | bytes] = {
+ "ModelID": self.fp.read(4)
+ }
+
+ self.fp.read(4)
+ # Seconds since 2000
+ camerainfo["TimeStamp"] = i32le(self.fp.read(12))
+
+ self.fp.read(4)
+ camerainfo["InternalSerialNumber"] = self.fp.read(4)
+
+ self.fp.read(12)
+ parallax = self.fp.read(4)
+ handler = ImageFileDirectory_v2._load_dispatch[
+ TiffTags.FLOAT
+ ][1]
+ camerainfo["Parallax"] = handler(
+ ImageFileDirectory_v2(), parallax, False
+ )[0]
+
+ self.fp.read(4)
+ camerainfo["Category"] = self.fp.read(2)
+
+ makernote = {0x1101: camerainfo}
+ self._ifds[tag] = makernote
+ except struct.error:
+ pass
+ else:
+ # Interop
+ ifd = self._get_ifd_dict(tag_data, tag)
+ if ifd is not None:
+ self._ifds[tag] = ifd
+ ifd = self._ifds.setdefault(tag, {})
+ if tag == ExifTags.IFD.Exif and self._hidden_data:
+ ifd = {
+ k: v
+ for (k, v) in ifd.items()
+ if k not in (ExifTags.IFD.Interop, ExifTags.IFD.MakerNote)
+ }
+ return ifd
+
+ def hide_offsets(self) -> None:
+ for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo):
+ if tag in self:
+ self._hidden_data[tag] = self[tag]
+ del self[tag]
+
+ def __str__(self) -> str:
+ if self._info is not None:
+ # Load all keys into self._data
+ for tag in self._info:
+ self[tag]
+
+ return str(self._data)
+
+ def __len__(self) -> int:
+ keys = set(self._data)
+ if self._info is not None:
+ keys.update(self._info)
+ return len(keys)
+
+ def __getitem__(self, tag: int) -> Any:
+ if self._info is not None and tag not in self._data and tag in self._info:
+ self._data[tag] = self._fixup(self._info[tag])
+ del self._info[tag]
+ return self._data[tag]
+
+ def __contains__(self, tag: object) -> bool:
+ return tag in self._data or (self._info is not None and tag in self._info)
+
+ def __setitem__(self, tag: int, value: Any) -> None:
+ if self._info is not None and tag in self._info:
+ del self._info[tag]
+ self._data[tag] = value
+
+ def __delitem__(self, tag: int) -> None:
+ if self._info is not None and tag in self._info:
+ del self._info[tag]
+ else:
+ del self._data[tag]
+ if tag in self._ifds:
+ del self._ifds[tag]
+
+ def __iter__(self) -> Iterator[int]:
+ keys = set(self._data)
+ if self._info is not None:
+ keys.update(self._info)
+ return iter(keys)
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageChops.py b/venv/lib/python3.11/site-packages/PIL/ImageChops.py
new file mode 100644
index 000000000..29a5c995f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageChops.py
@@ -0,0 +1,311 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# standard channel operations
+#
+# History:
+# 1996-03-24 fl Created
+# 1996-08-13 fl Added logical operations (for "1" images)
+# 2000-10-12 fl Added offset method (from Image.py)
+#
+# Copyright (c) 1997-2000 by Secret Labs AB
+# Copyright (c) 1996-2000 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+from __future__ import annotations
+
+from . import Image
+
+
+def constant(image: Image.Image, value: int) -> Image.Image:
+ """Fill a channel with a given gray level.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return Image.new("L", image.size, value)
+
+
+def duplicate(image: Image.Image) -> Image.Image:
+ """Copy a channel. Alias for :py:meth:`PIL.Image.Image.copy`.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return image.copy()
+
+
+def invert(image: Image.Image) -> Image.Image:
+ """
+ Invert an image (channel). ::
+
+ out = MAX - image
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image.load()
+ return image._new(image.im.chop_invert())
+
+
+def lighter(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Compares the two images, pixel by pixel, and returns a new image containing
+ the lighter values. ::
+
+ out = max(image1, image2)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_lighter(image2.im))
+
+
+def darker(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Compares the two images, pixel by pixel, and returns a new image containing
+ the darker values. ::
+
+ out = min(image1, image2)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_darker(image2.im))
+
+
+def difference(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Returns the absolute value of the pixel-by-pixel difference between the two
+ images. ::
+
+ out = abs(image1 - image2)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_difference(image2.im))
+
+
+def multiply(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Superimposes two images on top of each other.
+
+ If you multiply an image with a solid black image, the result is black. If
+ you multiply with a solid white image, the image is unaffected. ::
+
+ out = image1 * image2 / MAX
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_multiply(image2.im))
+
+
+def screen(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Superimposes two inverted images on top of each other. ::
+
+ out = MAX - ((MAX - image1) * (MAX - image2) / MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_screen(image2.im))
+
+
+def soft_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Superimposes two images on top of each other using the Soft Light algorithm
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_soft_light(image2.im))
+
+
+def hard_light(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Superimposes two images on top of each other using the Hard Light algorithm
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_hard_light(image2.im))
+
+
+def overlay(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """
+ Superimposes two images on top of each other using the Overlay algorithm
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_overlay(image2.im))
+
+
+def add(
+ image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
+) -> Image.Image:
+ """
+ Adds two images, dividing the result by scale and adding the
+ offset. If omitted, scale defaults to 1.0, and offset to 0.0. ::
+
+ out = ((image1 + image2) / scale + offset)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_add(image2.im, scale, offset))
+
+
+def subtract(
+ image1: Image.Image, image2: Image.Image, scale: float = 1.0, offset: float = 0
+) -> Image.Image:
+ """
+ Subtracts two images, dividing the result by scale and adding the offset.
+ If omitted, scale defaults to 1.0, and offset to 0.0. ::
+
+ out = ((image1 - image2) / scale + offset)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_subtract(image2.im, scale, offset))
+
+
+def add_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """Add two images, without clipping the result. ::
+
+ out = ((image1 + image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_add_modulo(image2.im))
+
+
+def subtract_modulo(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """Subtract two images, without clipping the result. ::
+
+ out = ((image1 - image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_subtract_modulo(image2.im))
+
+
+def logical_and(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """Logical AND between two images.
+
+ Both of the images must have mode "1". If you would like to perform a
+ logical AND on an image with a mode other than "1", try
+ :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask
+ as the second image. ::
+
+ out = ((image1 and image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_and(image2.im))
+
+
+def logical_or(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """Logical OR between two images.
+
+ Both of the images must have mode "1". ::
+
+ out = ((image1 or image2) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_or(image2.im))
+
+
+def logical_xor(image1: Image.Image, image2: Image.Image) -> Image.Image:
+ """Logical XOR between two images.
+
+ Both of the images must have mode "1". ::
+
+ out = ((bool(image1) != bool(image2)) % MAX)
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ image1.load()
+ image2.load()
+ return image1._new(image1.im.chop_xor(image2.im))
+
+
+def blend(image1: Image.Image, image2: Image.Image, alpha: float) -> Image.Image:
+ """Blend images using constant transparency weight. Alias for
+ :py:func:`PIL.Image.blend`.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return Image.blend(image1, image2, alpha)
+
+
+def composite(
+ image1: Image.Image, image2: Image.Image, mask: Image.Image
+) -> Image.Image:
+ """Create composite using transparency mask. Alias for
+ :py:func:`PIL.Image.composite`.
+
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ return Image.composite(image1, image2, mask)
+
+
+def offset(image: Image.Image, xoffset: int, yoffset: int | None = None) -> Image.Image:
+ """Returns a copy of the image where data has been offset by the given
+ distances. Data wraps around the edges. If ``yoffset`` is omitted, it
+ is assumed to be equal to ``xoffset``.
+
+ :param image: Input image.
+ :param xoffset: The horizontal distance.
+ :param yoffset: The vertical distance. If omitted, both
+ distances are set to the same value.
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+
+ if yoffset is None:
+ yoffset = xoffset
+ image.load()
+ return image._new(image.im.offset(xoffset, yoffset))
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageCms.py b/venv/lib/python3.11/site-packages/PIL/ImageCms.py
new file mode 100644
index 000000000..388a9296f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageCms.py
@@ -0,0 +1,1078 @@
+# The Python Imaging Library.
+# $Id$
+
+# Optional color management support, based on Kevin Cazabon's PyCMS
+# library.
+
+# Originally released under LGPL. Graciously donated to PIL in
+# March 2009, for distribution under the standard PIL license
+
+# History:
+
+# 2009-03-08 fl Added to PIL.
+
+# Copyright (C) 2002-2003 Kevin Cazabon
+# Copyright (c) 2009 by Fredrik Lundh
+# Copyright (c) 2013 by Eric Soroos
+
+# See the README file for information on usage and redistribution. See
+# below for the original description.
+from __future__ import annotations
+
+import operator
+import sys
+from enum import IntEnum, IntFlag
+from functools import reduce
+from typing import Any, Literal, SupportsFloat, SupportsInt, Union
+
+from . import Image
+from ._deprecate import deprecate
+from ._typing import SupportsRead
+
+try:
+ from . import _imagingcms as core
+
+ _CmsProfileCompatible = Union[
+ str, SupportsRead[bytes], core.CmsProfile, "ImageCmsProfile"
+ ]
+except ImportError as ex:
+ # Allow error import for doc purposes, but error out when accessing
+ # anything in core.
+ from ._util import DeferredError
+
+ core = DeferredError.new(ex)
+
+_DESCRIPTION = """
+pyCMS
+
+ a Python / PIL interface to the littleCMS ICC Color Management System
+ Copyright (C) 2002-2003 Kevin Cazabon
+ kevin@cazabon.com
+ https://www.cazabon.com
+
+ pyCMS home page: https://www.cazabon.com/pyCMS
+ littleCMS home page: https://www.littlecms.com
+ (littleCMS is Copyright (C) 1998-2001 Marti Maria)
+
+ Originally released under LGPL. Graciously donated to PIL in
+ March 2009, for distribution under the standard PIL license
+
+ The pyCMS.py module provides a "clean" interface between Python/PIL and
+ pyCMSdll, taking care of some of the more complex handling of the direct
+ pyCMSdll functions, as well as error-checking and making sure that all
+ relevant data is kept together.
+
+ While it is possible to call pyCMSdll functions directly, it's not highly
+ recommended.
+
+ Version History:
+
+ 1.0.0 pil Oct 2013 Port to LCMS 2.
+
+ 0.1.0 pil mod March 10, 2009
+
+ Renamed display profile to proof profile. The proof
+ profile is the profile of the device that is being
+ simulated, not the profile of the device which is
+ actually used to display/print the final simulation
+ (that'd be the output profile) - also see LCMSAPI.txt
+ input colorspace -> using 'renderingIntent' -> proof
+ colorspace -> using 'proofRenderingIntent' -> output
+ colorspace
+
+ Added LCMS FLAGS support.
+ Added FLAGS["SOFTPROOFING"] as default flag for
+ buildProofTransform (otherwise the proof profile/intent
+ would be ignored).
+
+ 0.1.0 pil March 2009 - added to PIL, as PIL.ImageCms
+
+ 0.0.2 alpha Jan 6, 2002
+
+ Added try/except statements around type() checks of
+ potential CObjects... Python won't let you use type()
+ on them, and raises a TypeError (stupid, if you ask
+ me!)
+
+ Added buildProofTransformFromOpenProfiles() function.
+ Additional fixes in DLL, see DLL code for details.
+
+ 0.0.1 alpha first public release, Dec. 26, 2002
+
+ Known to-do list with current version (of Python interface, not pyCMSdll):
+
+ none
+
+"""
+
+_VERSION = "1.0.0 pil"
+
+
+# --------------------------------------------------------------------.
+
+
+#
+# intent/direction values
+
+
+class Intent(IntEnum):
+ PERCEPTUAL = 0
+ RELATIVE_COLORIMETRIC = 1
+ SATURATION = 2
+ ABSOLUTE_COLORIMETRIC = 3
+
+
+class Direction(IntEnum):
+ INPUT = 0
+ OUTPUT = 1
+ PROOF = 2
+
+
+#
+# flags
+
+
+class Flags(IntFlag):
+ """Flags and documentation are taken from ``lcms2.h``."""
+
+ NONE = 0
+ NOCACHE = 0x0040
+ """Inhibit 1-pixel cache"""
+ NOOPTIMIZE = 0x0100
+ """Inhibit optimizations"""
+ NULLTRANSFORM = 0x0200
+ """Don't transform anyway"""
+ GAMUTCHECK = 0x1000
+ """Out of Gamut alarm"""
+ SOFTPROOFING = 0x4000
+ """Do softproofing"""
+ BLACKPOINTCOMPENSATION = 0x2000
+ NOWHITEONWHITEFIXUP = 0x0004
+ """Don't fix scum dot"""
+ HIGHRESPRECALC = 0x0400
+ """Use more memory to give better accuracy"""
+ LOWRESPRECALC = 0x0800
+ """Use less memory to minimize resources"""
+ # this should be 8BITS_DEVICELINK, but that is not a valid name in Python:
+ USE_8BITS_DEVICELINK = 0x0008
+ """Create 8 bits devicelinks"""
+ GUESSDEVICECLASS = 0x0020
+ """Guess device class (for ``transform2devicelink``)"""
+ KEEP_SEQUENCE = 0x0080
+ """Keep profile sequence for devicelink creation"""
+ FORCE_CLUT = 0x0002
+ """Force CLUT optimization"""
+ CLUT_POST_LINEARIZATION = 0x0001
+ """create postlinearization tables if possible"""
+ CLUT_PRE_LINEARIZATION = 0x0010
+ """create prelinearization tables if possible"""
+ NONEGATIVES = 0x8000
+ """Prevent negative numbers in floating point transforms"""
+ COPY_ALPHA = 0x04000000
+ """Alpha channels are copied on ``cmsDoTransform()``"""
+ NODEFAULTRESOURCEDEF = 0x01000000
+
+ _GRIDPOINTS_1 = 1 << 16
+ _GRIDPOINTS_2 = 2 << 16
+ _GRIDPOINTS_4 = 4 << 16
+ _GRIDPOINTS_8 = 8 << 16
+ _GRIDPOINTS_16 = 16 << 16
+ _GRIDPOINTS_32 = 32 << 16
+ _GRIDPOINTS_64 = 64 << 16
+ _GRIDPOINTS_128 = 128 << 16
+
+ @staticmethod
+ def GRIDPOINTS(n: int) -> Flags:
+ """
+ Fine-tune control over number of gridpoints
+
+ :param n: :py:class:`int` in range ``0 <= n <= 255``
+ """
+ return Flags.NONE | ((n & 0xFF) << 16)
+
+
+_MAX_FLAG = reduce(operator.or_, Flags)
+
+
+_FLAGS = {
+ "MATRIXINPUT": 1,
+ "MATRIXOUTPUT": 2,
+ "MATRIXONLY": (1 | 2),
+ "NOWHITEONWHITEFIXUP": 4, # Don't hot fix scum dot
+ # Don't create prelinearization tables on precalculated transforms
+ # (internal use):
+ "NOPRELINEARIZATION": 16,
+ "GUESSDEVICECLASS": 32, # Guess device class (for transform2devicelink)
+ "NOTCACHE": 64, # Inhibit 1-pixel cache
+ "NOTPRECALC": 256,
+ "NULLTRANSFORM": 512, # Don't transform anyway
+ "HIGHRESPRECALC": 1024, # Use more memory to give better accuracy
+ "LOWRESPRECALC": 2048, # Use less memory to minimize resources
+ "WHITEBLACKCOMPENSATION": 8192,
+ "BLACKPOINTCOMPENSATION": 8192,
+ "GAMUTCHECK": 4096, # Out of Gamut alarm
+ "SOFTPROOFING": 16384, # Do softproofing
+ "PRESERVEBLACK": 32768, # Black preservation
+ "NODEFAULTRESOURCEDEF": 16777216, # CRD special
+ "GRIDPOINTS": lambda n: (n & 0xFF) << 16, # Gridpoints
+}
+
+
+# --------------------------------------------------------------------.
+# Experimental PIL-level API
+# --------------------------------------------------------------------.
+
+##
+# Profile.
+
+
+class ImageCmsProfile:
+ def __init__(self, profile: str | SupportsRead[bytes] | core.CmsProfile) -> None:
+ """
+ :param profile: Either a string representing a filename,
+ a file like object containing a profile or a
+ low-level profile object
+
+ """
+ self.filename: str | None = None
+
+ if isinstance(profile, str):
+ if sys.platform == "win32":
+ profile_bytes_path = profile.encode()
+ try:
+ profile_bytes_path.decode("ascii")
+ except UnicodeDecodeError:
+ with open(profile, "rb") as f:
+ self.profile = core.profile_frombytes(f.read())
+ return
+ self.filename = profile
+ self.profile = core.profile_open(profile)
+ elif hasattr(profile, "read"):
+ self.profile = core.profile_frombytes(profile.read())
+ elif isinstance(profile, core.CmsProfile):
+ self.profile = profile
+ else:
+ msg = "Invalid type for Profile" # type: ignore[unreachable]
+ raise TypeError(msg)
+
+ def __getattr__(self, name: str) -> Any:
+ if name in ("product_name", "product_info"):
+ deprecate(f"ImageCms.ImageCmsProfile.{name}", 13)
+ return None
+ msg = f"'{self.__class__.__name__}' object has no attribute '{name}'"
+ raise AttributeError(msg)
+
+ def tobytes(self) -> bytes:
+ """
+ Returns the profile in a format suitable for embedding in
+ saved images.
+
+ :returns: a bytes object containing the ICC profile.
+ """
+
+ return core.profile_tobytes(self.profile)
+
+
+class ImageCmsTransform(Image.ImagePointHandler):
+ """
+ Transform. This can be used with the procedural API, or with the standard
+ :py:func:`~PIL.Image.Image.point` method.
+
+ Will return the output profile in the ``output.info['icc_profile']``.
+ """
+
+ def __init__(
+ self,
+ input: ImageCmsProfile,
+ output: ImageCmsProfile,
+ input_mode: str,
+ output_mode: str,
+ intent: Intent = Intent.PERCEPTUAL,
+ proof: ImageCmsProfile | None = None,
+ proof_intent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
+ flags: Flags = Flags.NONE,
+ ):
+ if proof is None:
+ self.transform = core.buildTransform(
+ input.profile, output.profile, input_mode, output_mode, intent, flags
+ )
+ else:
+ self.transform = core.buildProofTransform(
+ input.profile,
+ output.profile,
+ proof.profile,
+ input_mode,
+ output_mode,
+ intent,
+ proof_intent,
+ flags,
+ )
+ # Note: inputMode and outputMode are for pyCMS compatibility only
+ self.input_mode = self.inputMode = input_mode
+ self.output_mode = self.outputMode = output_mode
+
+ self.output_profile = output
+
+ def point(self, im: Image.Image) -> Image.Image:
+ return self.apply(im)
+
+ def apply(self, im: Image.Image, imOut: Image.Image | None = None) -> Image.Image:
+ if im.mode != self.input_mode:
+ msg = "mode mismatch"
+ raise ValueError(msg)
+ if imOut is not None:
+ if imOut.mode != self.output_mode:
+ msg = "mode mismatch"
+ raise ValueError(msg)
+ else:
+ imOut = Image.new(self.output_mode, im.size, None)
+ self.transform.apply(im.getim(), imOut.getim())
+ imOut.info["icc_profile"] = self.output_profile.tobytes()
+ return imOut
+
+ def apply_in_place(self, im: Image.Image) -> Image.Image:
+ return self.apply(im, im)
+
+
+def get_display_profile(handle: SupportsInt | None = None) -> ImageCmsProfile | None:
+ """
+ (experimental) Fetches the profile for the current display device.
+
+ :returns: ``None`` if the profile is not known.
+ """
+
+ if sys.platform != "win32":
+ return None
+
+ from . import ImageWin # type: ignore[unused-ignore, unreachable]
+
+ if isinstance(handle, ImageWin.HDC):
+ profile = core.get_display_profile_win32(int(handle), 1)
+ else:
+ profile = core.get_display_profile_win32(int(handle or 0))
+ if profile is None:
+ return None
+ return ImageCmsProfile(profile)
+
+
+# --------------------------------------------------------------------.
+# pyCMS compatible layer
+# --------------------------------------------------------------------.
+
+
+class PyCMSError(Exception):
+ """(pyCMS) Exception class.
+ This is used for all errors in the pyCMS API."""
+
+ pass
+
+
+def profileToProfile(
+ im: Image.Image,
+ inputProfile: _CmsProfileCompatible,
+ outputProfile: _CmsProfileCompatible,
+ renderingIntent: Intent = Intent.PERCEPTUAL,
+ outputMode: str | None = None,
+ inPlace: bool = False,
+ flags: Flags = Flags.NONE,
+) -> Image.Image | None:
+ """
+ (pyCMS) Applies an ICC transformation to a given image, mapping from
+ ``inputProfile`` to ``outputProfile``.
+
+ If the input or output profiles specified are not valid filenames, a
+ :exc:`PyCMSError` will be raised. If ``inPlace`` is ``True`` and
+ ``outputMode != im.mode``, a :exc:`PyCMSError` will be raised.
+ If an error occurs during application of the profiles,
+ a :exc:`PyCMSError` will be raised.
+ If ``outputMode`` is not a mode supported by the ``outputProfile`` (or by pyCMS),
+ a :exc:`PyCMSError` will be raised.
+
+ This function applies an ICC transformation to im from ``inputProfile``'s
+ color space to ``outputProfile``'s color space using the specified rendering
+ intent to decide how to handle out-of-gamut colors.
+
+ ``outputMode`` can be used to specify that a color mode conversion is to
+ be done using these profiles, but the specified profiles must be able
+ to handle that mode. I.e., if converting im from RGB to CMYK using
+ profiles, the input profile must handle RGB data, and the output
+ profile must handle CMYK data.
+
+ :param im: An open :py:class:`~PIL.Image.Image` object (i.e. Image.new(...)
+ or Image.open(...), etc.)
+ :param inputProfile: String, as a valid filename path to the ICC input
+ profile you wish to use for this image, or a profile object
+ :param outputProfile: String, as a valid filename path to the ICC output
+ profile you wish to use for this image, or a profile object
+ :param renderingIntent: Integer (0-3) specifying the rendering intent you
+ wish to use for the transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param outputMode: A valid PIL mode for the output image (i.e. "RGB",
+ "CMYK", etc.). Note: if rendering the image "inPlace", outputMode
+ MUST be the same mode as the input, or omitted completely. If
+ omitted, the outputMode will be the same as the mode of the input
+ image (im.mode)
+ :param inPlace: Boolean. If ``True``, the original image is modified in-place,
+ and ``None`` is returned. If ``False`` (default), a new
+ :py:class:`~PIL.Image.Image` object is returned with the transform applied.
+ :param flags: Integer (0-...) specifying additional flags
+ :returns: Either None or a new :py:class:`~PIL.Image.Image` object, depending on
+ the value of ``inPlace``
+ :exception PyCMSError:
+ """
+
+ if outputMode is None:
+ outputMode = im.mode
+
+ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
+
+ if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
+ raise PyCMSError(msg)
+
+ try:
+ if not isinstance(inputProfile, ImageCmsProfile):
+ inputProfile = ImageCmsProfile(inputProfile)
+ if not isinstance(outputProfile, ImageCmsProfile):
+ outputProfile = ImageCmsProfile(outputProfile)
+ transform = ImageCmsTransform(
+ inputProfile,
+ outputProfile,
+ im.mode,
+ outputMode,
+ renderingIntent,
+ flags=flags,
+ )
+ if inPlace:
+ transform.apply_in_place(im)
+ imOut = None
+ else:
+ imOut = transform.apply(im)
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+ return imOut
+
+
+def getOpenProfile(
+ profileFilename: str | SupportsRead[bytes] | core.CmsProfile,
+) -> ImageCmsProfile:
+ """
+ (pyCMS) Opens an ICC profile file.
+
+ The PyCMSProfile object can be passed back into pyCMS for use in creating
+ transforms and such (as in ImageCms.buildTransformFromOpenProfiles()).
+
+ If ``profileFilename`` is not a valid filename for an ICC profile,
+ a :exc:`PyCMSError` will be raised.
+
+ :param profileFilename: String, as a valid filename path to the ICC profile
+ you wish to open, or a file-like object.
+ :returns: A CmsProfile class object.
+ :exception PyCMSError:
+ """
+
+ try:
+ return ImageCmsProfile(profileFilename)
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def buildTransform(
+ inputProfile: _CmsProfileCompatible,
+ outputProfile: _CmsProfileCompatible,
+ inMode: str,
+ outMode: str,
+ renderingIntent: Intent = Intent.PERCEPTUAL,
+ flags: Flags = Flags.NONE,
+) -> ImageCmsTransform:
+ """
+ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
+ ``outputProfile``. Use applyTransform to apply the transform to a given
+ image.
+
+ If the input or output profiles specified are not valid filenames, a
+ :exc:`PyCMSError` will be raised. If an error occurs during creation
+ of the transform, a :exc:`PyCMSError` will be raised.
+
+ If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile``
+ (or by pyCMS), a :exc:`PyCMSError` will be raised.
+
+ This function builds and returns an ICC transform from the ``inputProfile``
+ to the ``outputProfile`` using the ``renderingIntent`` to determine what to do
+ with out-of-gamut colors. It will ONLY work for converting images that
+ are in ``inMode`` to images that are in ``outMode`` color format (PIL mode,
+ i.e. "RGB", "RGBA", "CMYK", etc.).
+
+ Building the transform is a fair part of the overhead in
+ ImageCms.profileToProfile(), so if you're planning on converting multiple
+ images using the same input/output settings, this can save you time.
+ Once you have a transform object, it can be used with
+ ImageCms.applyProfile() to convert images without the need to re-compute
+ the lookup table for the transform.
+
+ The reason pyCMS returns a class object rather than a handle directly
+ to the transform is that it needs to keep track of the PIL input/output
+ modes that the transform is meant for. These attributes are stored in
+ the ``inMode`` and ``outMode`` attributes of the object (which can be
+ manually overridden if you really want to, but I don't know of any
+ time that would be of use, or would even work).
+
+ :param inputProfile: String, as a valid filename path to the ICC input
+ profile you wish to use for this transform, or a profile object
+ :param outputProfile: String, as a valid filename path to the ICC output
+ profile you wish to use for this transform, or a profile object
+ :param inMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param outMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param renderingIntent: Integer (0-3) specifying the rendering intent you
+ wish to use for the transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param flags: Integer (0-...) specifying additional flags
+ :returns: A CmsTransform class object.
+ :exception PyCMSError:
+ """
+
+ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
+
+ if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
+ raise PyCMSError(msg)
+
+ try:
+ if not isinstance(inputProfile, ImageCmsProfile):
+ inputProfile = ImageCmsProfile(inputProfile)
+ if not isinstance(outputProfile, ImageCmsProfile):
+ outputProfile = ImageCmsProfile(outputProfile)
+ return ImageCmsTransform(
+ inputProfile, outputProfile, inMode, outMode, renderingIntent, flags=flags
+ )
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def buildProofTransform(
+ inputProfile: _CmsProfileCompatible,
+ outputProfile: _CmsProfileCompatible,
+ proofProfile: _CmsProfileCompatible,
+ inMode: str,
+ outMode: str,
+ renderingIntent: Intent = Intent.PERCEPTUAL,
+ proofRenderingIntent: Intent = Intent.ABSOLUTE_COLORIMETRIC,
+ flags: Flags = Flags.SOFTPROOFING,
+) -> ImageCmsTransform:
+ """
+ (pyCMS) Builds an ICC transform mapping from the ``inputProfile`` to the
+ ``outputProfile``, but tries to simulate the result that would be
+ obtained on the ``proofProfile`` device.
+
+ If the input, output, or proof profiles specified are not valid
+ filenames, a :exc:`PyCMSError` will be raised.
+
+ If an error occurs during creation of the transform,
+ a :exc:`PyCMSError` will be raised.
+
+ If ``inMode`` or ``outMode`` are not a mode supported by the ``outputProfile``
+ (or by pyCMS), a :exc:`PyCMSError` will be raised.
+
+ This function builds and returns an ICC transform from the ``inputProfile``
+ to the ``outputProfile``, but tries to simulate the result that would be
+ obtained on the ``proofProfile`` device using ``renderingIntent`` and
+ ``proofRenderingIntent`` to determine what to do with out-of-gamut
+ colors. This is known as "soft-proofing". It will ONLY work for
+ converting images that are in ``inMode`` to images that are in outMode
+ color format (PIL mode, i.e. "RGB", "RGBA", "CMYK", etc.).
+
+ Usage of the resulting transform object is exactly the same as with
+ ImageCms.buildTransform().
+
+ Proof profiling is generally used when using an output device to get a
+ good idea of what the final printed/displayed image would look like on
+ the ``proofProfile`` device when it's quicker and easier to use the
+ output device for judging color. Generally, this means that the
+ output device is a monitor, or a dye-sub printer (etc.), and the simulated
+ device is something more expensive, complicated, or time consuming
+ (making it difficult to make a real print for color judgement purposes).
+
+ Soft-proofing basically functions by adjusting the colors on the
+ output device to match the colors of the device being simulated. However,
+ when the simulated device has a much wider gamut than the output
+ device, you may obtain marginal results.
+
+ :param inputProfile: String, as a valid filename path to the ICC input
+ profile you wish to use for this transform, or a profile object
+ :param outputProfile: String, as a valid filename path to the ICC output
+ (monitor, usually) profile you wish to use for this transform, or a
+ profile object
+ :param proofProfile: String, as a valid filename path to the ICC proof
+ profile you wish to use for this transform, or a profile object
+ :param inMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param outMode: String, as a valid PIL mode that the appropriate profile
+ also supports (i.e. "RGB", "RGBA", "CMYK", etc.)
+ :param renderingIntent: Integer (0-3) specifying the rendering intent you
+ wish to use for the input->proof (simulated) transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param proofRenderingIntent: Integer (0-3) specifying the rendering intent
+ you wish to use for proof->output transform
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param flags: Integer (0-...) specifying additional flags
+ :returns: A CmsTransform class object.
+ :exception PyCMSError:
+ """
+
+ if not isinstance(renderingIntent, int) or not (0 <= renderingIntent <= 3):
+ msg = "renderingIntent must be an integer between 0 and 3"
+ raise PyCMSError(msg)
+
+ if not isinstance(flags, int) or not (0 <= flags <= _MAX_FLAG):
+ msg = f"flags must be an integer between 0 and {_MAX_FLAG}"
+ raise PyCMSError(msg)
+
+ try:
+ if not isinstance(inputProfile, ImageCmsProfile):
+ inputProfile = ImageCmsProfile(inputProfile)
+ if not isinstance(outputProfile, ImageCmsProfile):
+ outputProfile = ImageCmsProfile(outputProfile)
+ if not isinstance(proofProfile, ImageCmsProfile):
+ proofProfile = ImageCmsProfile(proofProfile)
+ return ImageCmsTransform(
+ inputProfile,
+ outputProfile,
+ inMode,
+ outMode,
+ renderingIntent,
+ proofProfile,
+ proofRenderingIntent,
+ flags,
+ )
+ except (OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+buildTransformFromOpenProfiles = buildTransform
+buildProofTransformFromOpenProfiles = buildProofTransform
+
+
+def applyTransform(
+ im: Image.Image, transform: ImageCmsTransform, inPlace: bool = False
+) -> Image.Image | None:
+ """
+ (pyCMS) Applies a transform to a given image.
+
+ If ``im.mode != transform.input_mode``, a :exc:`PyCMSError` is raised.
+
+ If ``inPlace`` is ``True`` and ``transform.input_mode != transform.output_mode``, a
+ :exc:`PyCMSError` is raised.
+
+ If ``im.mode``, ``transform.input_mode`` or ``transform.output_mode`` is not
+ supported by pyCMSdll or the profiles you used for the transform, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while the transform is being applied,
+ a :exc:`PyCMSError` is raised.
+
+ This function applies a pre-calculated transform (from
+ ImageCms.buildTransform() or ImageCms.buildTransformFromOpenProfiles())
+ to an image. The transform can be used for multiple images, saving
+ considerable calculation time if doing the same conversion multiple times.
+
+ If you want to modify im in-place instead of receiving a new image as
+ the return value, set ``inPlace`` to ``True``. This can only be done if
+ ``transform.input_mode`` and ``transform.output_mode`` are the same, because we
+ can't change the mode in-place (the buffer sizes for some modes are
+ different). The default behavior is to return a new :py:class:`~PIL.Image.Image`
+ object of the same dimensions in mode ``transform.output_mode``.
+
+ :param im: An :py:class:`~PIL.Image.Image` object, and ``im.mode`` must be the same
+ as the ``input_mode`` supported by the transform.
+ :param transform: A valid CmsTransform class object
+ :param inPlace: Bool. If ``True``, ``im`` is modified in place and ``None`` is
+ returned, if ``False``, a new :py:class:`~PIL.Image.Image` object with the
+ transform applied is returned (and ``im`` is not changed). The default is
+ ``False``.
+ :returns: Either ``None``, or a new :py:class:`~PIL.Image.Image` object,
+ depending on the value of ``inPlace``. The profile will be returned in
+ the image's ``info['icc_profile']``.
+ :exception PyCMSError:
+ """
+
+ try:
+ if inPlace:
+ transform.apply_in_place(im)
+ imOut = None
+ else:
+ imOut = transform.apply(im)
+ except (TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+ return imOut
+
+
+def createProfile(
+ colorSpace: Literal["LAB", "XYZ", "sRGB"], colorTemp: SupportsFloat = 0
+) -> core.CmsProfile:
+ """
+ (pyCMS) Creates a profile.
+
+ If colorSpace not in ``["LAB", "XYZ", "sRGB"]``,
+ a :exc:`PyCMSError` is raised.
+
+ If using LAB and ``colorTemp`` is not a positive integer,
+ a :exc:`PyCMSError` is raised.
+
+ If an error occurs while creating the profile,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to create common profiles on-the-fly instead of
+ having to supply a profile on disk and knowing the path to it. It
+ returns a normal CmsProfile object that can be passed to
+ ImageCms.buildTransformFromOpenProfiles() to create a transform to apply
+ to images.
+
+ :param colorSpace: String, the color space of the profile you wish to
+ create.
+ Currently only "LAB", "XYZ", and "sRGB" are supported.
+ :param colorTemp: Positive number for the white point for the profile, in
+ degrees Kelvin (i.e. 5000, 6500, 9600, etc.). The default is for D50
+ illuminant if omitted (5000k). colorTemp is ONLY applied to LAB
+ profiles, and is ignored for XYZ and sRGB.
+ :returns: A CmsProfile class object
+ :exception PyCMSError:
+ """
+
+ if colorSpace not in ["LAB", "XYZ", "sRGB"]:
+ msg = (
+ f"Color space not supported for on-the-fly profile creation ({colorSpace})"
+ )
+ raise PyCMSError(msg)
+
+ if colorSpace == "LAB":
+ try:
+ colorTemp = float(colorTemp)
+ except (TypeError, ValueError) as e:
+ msg = f'Color temperature must be numeric, "{colorTemp}" not valid'
+ raise PyCMSError(msg) from e
+
+ try:
+ return core.createProfile(colorSpace, colorTemp)
+ except (TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileName(profile: _CmsProfileCompatible) -> str:
+ """
+
+ (pyCMS) Gets the internal product name for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile,
+ a :exc:`PyCMSError` is raised If an error occurs while trying
+ to obtain the name tag, a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the INTERNAL name of the profile (stored
+ in an ICC tag in the profile itself), usually the one used when the
+ profile was originally created. Sometimes this tag also contains
+ additional information supplied by the creator.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal name of the profile as stored
+ in an ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ # do it in python, not c.
+ # // name was "%s - %s" (model, manufacturer) || Description ,
+ # // but if the Model and Manufacturer were the same or the model
+ # // was long, Just the model, in 1.x
+ model = profile.profile.model
+ manufacturer = profile.profile.manufacturer
+
+ if not (model or manufacturer):
+ return (profile.profile.profile_description or "") + "\n"
+ if not manufacturer or (model and len(model) > 30):
+ return f"{model}\n"
+ return f"{model} - {manufacturer}\n"
+
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileInfo(profile: _CmsProfileCompatible) -> str:
+ """
+ (pyCMS) Gets the internal product information for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile,
+ a :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the info tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ info tag. This often contains details about the profile, and how it
+ was created, as supplied by the creator.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ # add an extra newline to preserve pyCMS compatibility
+ # Python, not C. the white point bits weren't working well,
+ # so skipping.
+ # info was description \r\n\r\n copyright \r\n\r\n K007 tag \r\n\r\n whitepoint
+ description = profile.profile.profile_description
+ cpright = profile.profile.copyright
+ elements = [element for element in (description, cpright) if element]
+ return "\r\n\r\n".join(elements) + "\r\n\r\n"
+
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileCopyright(profile: _CmsProfileCompatible) -> str:
+ """
+ (pyCMS) Gets the copyright for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the copyright tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ copyright tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.copyright or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileManufacturer(profile: _CmsProfileCompatible) -> str:
+ """
+ (pyCMS) Gets the manufacturer for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the manufacturer tag, a
+ :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ manufacturer tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.manufacturer or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileModel(profile: _CmsProfileCompatible) -> str:
+ """
+ (pyCMS) Gets the model for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the model tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ model tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in
+ an ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.model or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getProfileDescription(profile: _CmsProfileCompatible) -> str:
+ """
+ (pyCMS) Gets the description for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the description tag,
+ a :exc:`PyCMSError` is raised.
+
+ Use this function to obtain the information stored in the profile's
+ description tag.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: A string containing the internal profile information stored in an
+ ICC tag.
+ :exception PyCMSError:
+ """
+
+ try:
+ # add an extra newline to preserve pyCMS compatibility
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return (profile.profile.profile_description or "") + "\n"
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def getDefaultIntent(profile: _CmsProfileCompatible) -> int:
+ """
+ (pyCMS) Gets the default intent name for the given profile.
+
+ If ``profile`` isn't a valid CmsProfile object or filename to a profile, a
+ :exc:`PyCMSError` is raised.
+
+ If an error occurs while trying to obtain the default intent, a
+ :exc:`PyCMSError` is raised.
+
+ Use this function to determine the default (and usually best optimized)
+ rendering intent for this profile. Most profiles support multiple
+ rendering intents, but are intended mostly for one type of conversion.
+ If you wish to use a different intent than returned, use
+ ImageCms.isIntentSupported() to verify it will work first.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :returns: Integer 0-3 specifying the default rendering intent for this
+ profile.
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :exception PyCMSError:
+ """
+
+ try:
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ return profile.profile.rendering_intent
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
+
+
+def isIntentSupported(
+ profile: _CmsProfileCompatible, intent: Intent, direction: Direction
+) -> Literal[-1, 1]:
+ """
+ (pyCMS) Checks if a given intent is supported.
+
+ Use this function to verify that you can use your desired
+ ``intent`` with ``profile``, and that ``profile`` can be used for the
+ input/output/proof profile as you desire.
+
+ Some profiles are created specifically for one "direction", can cannot
+ be used for others. Some profiles can only be used for certain
+ rendering intents, so it's best to either verify this before trying
+ to create a transform with them (using this function), or catch the
+ potential :exc:`PyCMSError` that will occur if they don't
+ support the modes you select.
+
+ :param profile: EITHER a valid CmsProfile object, OR a string of the
+ filename of an ICC profile.
+ :param intent: Integer (0-3) specifying the rendering intent you wish to
+ use with this profile
+
+ ImageCms.Intent.PERCEPTUAL = 0 (DEFAULT)
+ ImageCms.Intent.RELATIVE_COLORIMETRIC = 1
+ ImageCms.Intent.SATURATION = 2
+ ImageCms.Intent.ABSOLUTE_COLORIMETRIC = 3
+
+ see the pyCMS documentation for details on rendering intents and what
+ they do.
+ :param direction: Integer specifying if the profile is to be used for
+ input, output, or proof
+
+ INPUT = 0 (or use ImageCms.Direction.INPUT)
+ OUTPUT = 1 (or use ImageCms.Direction.OUTPUT)
+ PROOF = 2 (or use ImageCms.Direction.PROOF)
+
+ :returns: 1 if the intent/direction are supported, -1 if they are not.
+ :exception PyCMSError:
+ """
+
+ try:
+ if not isinstance(profile, ImageCmsProfile):
+ profile = ImageCmsProfile(profile)
+ # FIXME: I get different results for the same data w. different
+ # compilers. Bug in LittleCMS or in the binding?
+ if profile.profile.is_intent_supported(intent, direction):
+ return 1
+ else:
+ return -1
+ except (AttributeError, OSError, TypeError, ValueError) as v:
+ raise PyCMSError(v) from v
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageColor.py b/venv/lib/python3.11/site-packages/PIL/ImageColor.py
new file mode 100644
index 000000000..9a15a8eb7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageColor.py
@@ -0,0 +1,320 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# map CSS3-style colour description strings to RGB
+#
+# History:
+# 2002-10-24 fl Added support for CSS-style color strings
+# 2002-12-15 fl Added RGBA support
+# 2004-03-27 fl Fixed remaining int() problems for Python 1.5.2
+# 2004-07-19 fl Fixed gray/grey spelling issues
+# 2009-03-05 fl Fixed rounding error in grayscale calculation
+#
+# Copyright (c) 2002-2004 by Secret Labs AB
+# Copyright (c) 2002-2004 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import re
+from functools import lru_cache
+
+from . import Image
+
+
+@lru_cache
+def getrgb(color: str) -> tuple[int, int, int] | tuple[int, int, int, int]:
+ """
+ Convert a color string to an RGB or RGBA tuple. If the string cannot be
+ parsed, this function raises a :py:exc:`ValueError` exception.
+
+ .. versionadded:: 1.1.4
+
+ :param color: A color string
+ :return: ``(red, green, blue[, alpha])``
+ """
+ if len(color) > 100:
+ msg = "color specifier is too long"
+ raise ValueError(msg)
+ color = color.lower()
+
+ rgb = colormap.get(color, None)
+ if rgb:
+ if isinstance(rgb, tuple):
+ return rgb
+ rgb_tuple = getrgb(rgb)
+ assert len(rgb_tuple) == 3
+ colormap[color] = rgb_tuple
+ return rgb_tuple
+
+ # check for known string formats
+ if re.match("#[a-f0-9]{3}$", color):
+ return int(color[1] * 2, 16), int(color[2] * 2, 16), int(color[3] * 2, 16)
+
+ if re.match("#[a-f0-9]{4}$", color):
+ return (
+ int(color[1] * 2, 16),
+ int(color[2] * 2, 16),
+ int(color[3] * 2, 16),
+ int(color[4] * 2, 16),
+ )
+
+ if re.match("#[a-f0-9]{6}$", color):
+ return int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)
+
+ if re.match("#[a-f0-9]{8}$", color):
+ return (
+ int(color[1:3], 16),
+ int(color[3:5], 16),
+ int(color[5:7], 16),
+ int(color[7:9], 16),
+ )
+
+ m = re.match(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
+ if m:
+ return int(m.group(1)), int(m.group(2)), int(m.group(3))
+
+ m = re.match(r"rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$", color)
+ if m:
+ return (
+ int((int(m.group(1)) * 255) / 100.0 + 0.5),
+ int((int(m.group(2)) * 255) / 100.0 + 0.5),
+ int((int(m.group(3)) * 255) / 100.0 + 0.5),
+ )
+
+ m = re.match(
+ r"hsl\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
+ )
+ if m:
+ from colorsys import hls_to_rgb
+
+ rgb_floats = hls_to_rgb(
+ float(m.group(1)) / 360.0,
+ float(m.group(3)) / 100.0,
+ float(m.group(2)) / 100.0,
+ )
+ return (
+ int(rgb_floats[0] * 255 + 0.5),
+ int(rgb_floats[1] * 255 + 0.5),
+ int(rgb_floats[2] * 255 + 0.5),
+ )
+
+ m = re.match(
+ r"hs[bv]\(\s*(\d+\.?\d*)\s*,\s*(\d+\.?\d*)%\s*,\s*(\d+\.?\d*)%\s*\)$", color
+ )
+ if m:
+ from colorsys import hsv_to_rgb
+
+ rgb_floats = hsv_to_rgb(
+ float(m.group(1)) / 360.0,
+ float(m.group(2)) / 100.0,
+ float(m.group(3)) / 100.0,
+ )
+ return (
+ int(rgb_floats[0] * 255 + 0.5),
+ int(rgb_floats[1] * 255 + 0.5),
+ int(rgb_floats[2] * 255 + 0.5),
+ )
+
+ m = re.match(r"rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$", color)
+ if m:
+ return int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))
+ msg = f"unknown color specifier: {repr(color)}"
+ raise ValueError(msg)
+
+
+@lru_cache
+def getcolor(color: str, mode: str) -> int | tuple[int, ...]:
+ """
+ Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if
+ ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is
+ not color or a palette image, converts the RGB value to a grayscale value.
+ If the string cannot be parsed, this function raises a :py:exc:`ValueError`
+ exception.
+
+ .. versionadded:: 1.1.4
+
+ :param color: A color string
+ :param mode: Convert result to this mode
+ :return: ``graylevel, (graylevel, alpha) or (red, green, blue[, alpha])``
+ """
+ # same as getrgb, but converts the result to the given mode
+ rgb, alpha = getrgb(color), 255
+ if len(rgb) == 4:
+ alpha = rgb[3]
+ rgb = rgb[:3]
+
+ if mode == "HSV":
+ from colorsys import rgb_to_hsv
+
+ r, g, b = rgb
+ h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255)
+ return int(h * 255), int(s * 255), int(v * 255)
+ elif Image.getmodebase(mode) == "L":
+ r, g, b = rgb
+ # ITU-R Recommendation 601-2 for nonlinear RGB
+ # scaled to 24 bits to match the convert's implementation.
+ graylevel = (r * 19595 + g * 38470 + b * 7471 + 0x8000) >> 16
+ if mode[-1] == "A":
+ return graylevel, alpha
+ return graylevel
+ elif mode[-1] == "A":
+ return rgb + (alpha,)
+ return rgb
+
+
+colormap: dict[str, str | tuple[int, int, int]] = {
+ # X11 colour table from https://drafts.csswg.org/css-color-4/, with
+ # gray/grey spelling issues fixed. This is a superset of HTML 4.0
+ # colour names used in CSS 1.
+ "aliceblue": "#f0f8ff",
+ "antiquewhite": "#faebd7",
+ "aqua": "#00ffff",
+ "aquamarine": "#7fffd4",
+ "azure": "#f0ffff",
+ "beige": "#f5f5dc",
+ "bisque": "#ffe4c4",
+ "black": "#000000",
+ "blanchedalmond": "#ffebcd",
+ "blue": "#0000ff",
+ "blueviolet": "#8a2be2",
+ "brown": "#a52a2a",
+ "burlywood": "#deb887",
+ "cadetblue": "#5f9ea0",
+ "chartreuse": "#7fff00",
+ "chocolate": "#d2691e",
+ "coral": "#ff7f50",
+ "cornflowerblue": "#6495ed",
+ "cornsilk": "#fff8dc",
+ "crimson": "#dc143c",
+ "cyan": "#00ffff",
+ "darkblue": "#00008b",
+ "darkcyan": "#008b8b",
+ "darkgoldenrod": "#b8860b",
+ "darkgray": "#a9a9a9",
+ "darkgrey": "#a9a9a9",
+ "darkgreen": "#006400",
+ "darkkhaki": "#bdb76b",
+ "darkmagenta": "#8b008b",
+ "darkolivegreen": "#556b2f",
+ "darkorange": "#ff8c00",
+ "darkorchid": "#9932cc",
+ "darkred": "#8b0000",
+ "darksalmon": "#e9967a",
+ "darkseagreen": "#8fbc8f",
+ "darkslateblue": "#483d8b",
+ "darkslategray": "#2f4f4f",
+ "darkslategrey": "#2f4f4f",
+ "darkturquoise": "#00ced1",
+ "darkviolet": "#9400d3",
+ "deeppink": "#ff1493",
+ "deepskyblue": "#00bfff",
+ "dimgray": "#696969",
+ "dimgrey": "#696969",
+ "dodgerblue": "#1e90ff",
+ "firebrick": "#b22222",
+ "floralwhite": "#fffaf0",
+ "forestgreen": "#228b22",
+ "fuchsia": "#ff00ff",
+ "gainsboro": "#dcdcdc",
+ "ghostwhite": "#f8f8ff",
+ "gold": "#ffd700",
+ "goldenrod": "#daa520",
+ "gray": "#808080",
+ "grey": "#808080",
+ "green": "#008000",
+ "greenyellow": "#adff2f",
+ "honeydew": "#f0fff0",
+ "hotpink": "#ff69b4",
+ "indianred": "#cd5c5c",
+ "indigo": "#4b0082",
+ "ivory": "#fffff0",
+ "khaki": "#f0e68c",
+ "lavender": "#e6e6fa",
+ "lavenderblush": "#fff0f5",
+ "lawngreen": "#7cfc00",
+ "lemonchiffon": "#fffacd",
+ "lightblue": "#add8e6",
+ "lightcoral": "#f08080",
+ "lightcyan": "#e0ffff",
+ "lightgoldenrodyellow": "#fafad2",
+ "lightgreen": "#90ee90",
+ "lightgray": "#d3d3d3",
+ "lightgrey": "#d3d3d3",
+ "lightpink": "#ffb6c1",
+ "lightsalmon": "#ffa07a",
+ "lightseagreen": "#20b2aa",
+ "lightskyblue": "#87cefa",
+ "lightslategray": "#778899",
+ "lightslategrey": "#778899",
+ "lightsteelblue": "#b0c4de",
+ "lightyellow": "#ffffe0",
+ "lime": "#00ff00",
+ "limegreen": "#32cd32",
+ "linen": "#faf0e6",
+ "magenta": "#ff00ff",
+ "maroon": "#800000",
+ "mediumaquamarine": "#66cdaa",
+ "mediumblue": "#0000cd",
+ "mediumorchid": "#ba55d3",
+ "mediumpurple": "#9370db",
+ "mediumseagreen": "#3cb371",
+ "mediumslateblue": "#7b68ee",
+ "mediumspringgreen": "#00fa9a",
+ "mediumturquoise": "#48d1cc",
+ "mediumvioletred": "#c71585",
+ "midnightblue": "#191970",
+ "mintcream": "#f5fffa",
+ "mistyrose": "#ffe4e1",
+ "moccasin": "#ffe4b5",
+ "navajowhite": "#ffdead",
+ "navy": "#000080",
+ "oldlace": "#fdf5e6",
+ "olive": "#808000",
+ "olivedrab": "#6b8e23",
+ "orange": "#ffa500",
+ "orangered": "#ff4500",
+ "orchid": "#da70d6",
+ "palegoldenrod": "#eee8aa",
+ "palegreen": "#98fb98",
+ "paleturquoise": "#afeeee",
+ "palevioletred": "#db7093",
+ "papayawhip": "#ffefd5",
+ "peachpuff": "#ffdab9",
+ "peru": "#cd853f",
+ "pink": "#ffc0cb",
+ "plum": "#dda0dd",
+ "powderblue": "#b0e0e6",
+ "purple": "#800080",
+ "rebeccapurple": "#663399",
+ "red": "#ff0000",
+ "rosybrown": "#bc8f8f",
+ "royalblue": "#4169e1",
+ "saddlebrown": "#8b4513",
+ "salmon": "#fa8072",
+ "sandybrown": "#f4a460",
+ "seagreen": "#2e8b57",
+ "seashell": "#fff5ee",
+ "sienna": "#a0522d",
+ "silver": "#c0c0c0",
+ "skyblue": "#87ceeb",
+ "slateblue": "#6a5acd",
+ "slategray": "#708090",
+ "slategrey": "#708090",
+ "snow": "#fffafa",
+ "springgreen": "#00ff7f",
+ "steelblue": "#4682b4",
+ "tan": "#d2b48c",
+ "teal": "#008080",
+ "thistle": "#d8bfd8",
+ "tomato": "#ff6347",
+ "turquoise": "#40e0d0",
+ "violet": "#ee82ee",
+ "wheat": "#f5deb3",
+ "white": "#ffffff",
+ "whitesmoke": "#f5f5f5",
+ "yellow": "#ffff00",
+ "yellowgreen": "#9acd32",
+}
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageDraw.py b/venv/lib/python3.11/site-packages/PIL/ImageDraw.py
new file mode 100644
index 000000000..561c44729
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageDraw.py
@@ -0,0 +1,1002 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# drawing interface operations
+#
+# History:
+# 1996-04-13 fl Created (experimental)
+# 1996-08-07 fl Filled polygons, ellipses.
+# 1996-08-13 fl Added text support
+# 1998-06-28 fl Handle I and F images
+# 1998-12-29 fl Added arc; use arc primitive to draw ellipses
+# 1999-01-10 fl Added shape stuff (experimental)
+# 1999-02-06 fl Added bitmap support
+# 1999-02-11 fl Changed all primitives to take options
+# 1999-02-20 fl Fixed backwards compatibility
+# 2000-10-12 fl Copy on write, when necessary
+# 2001-02-18 fl Use default ink for bitmap/text also in fill mode
+# 2002-10-24 fl Added support for CSS-style color strings
+# 2002-12-10 fl Added experimental support for RGBA-on-RGB drawing
+# 2002-12-11 fl Refactored low-level drawing API (work in progress)
+# 2004-08-26 fl Made Draw() a factory function, added getdraw() support
+# 2004-09-04 fl Added width support to line primitive
+# 2004-09-10 fl Added font mode handling
+# 2006-06-19 fl Added font bearing support (getmask2)
+#
+# Copyright (c) 1997-2006 by Secret Labs AB
+# Copyright (c) 1996-2006 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import math
+import struct
+from collections.abc import Sequence
+from typing import cast
+
+from . import Image, ImageColor, ImageFont, ImageText
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from types import ModuleType
+ from typing import Any, AnyStr
+
+ from . import ImageDraw2
+ from ._typing import Coords, _Ink
+
+# experimental access to the outline API
+Outline: Callable[[], Image.core._Outline] = Image.core.outline
+
+"""
+A simple 2D drawing interface for PIL images.
+
+Application code should use the Draw factory, instead of
+directly.
+"""
+
+
+class ImageDraw:
+ font: ImageFont.BaseImageFont | None = None
+
+ def __init__(self, im: Image.Image, mode: str | None = None) -> None:
+ """
+ Create a drawing instance.
+
+ :param im: The image to draw in.
+ :param mode: Optional mode to use for color values. For RGB
+ images, this argument can be RGB or RGBA (to blend the
+ drawing into the image). For all other modes, this argument
+ must be the same as the image mode. If omitted, the mode
+ defaults to the mode of the image.
+ """
+ im._ensure_mutable()
+ blend = 0
+ if mode is None:
+ mode = im.mode
+ if mode != im.mode:
+ if mode == "RGBA" and im.mode == "RGB":
+ blend = 1
+ else:
+ msg = "mode mismatch"
+ raise ValueError(msg)
+ if mode == "P":
+ self.palette = im.palette
+ else:
+ self.palette = None
+ self._image = im
+ self.im = im.im
+ self.draw = Image.core.draw(self.im, blend)
+ self.mode = mode
+ if mode in ("I", "F"):
+ self.ink = self.draw.draw_ink(1)
+ else:
+ self.ink = self.draw.draw_ink(-1)
+ if mode in ("1", "P", "I", "F"):
+ # FIXME: fix Fill2 to properly support matte for I+F images
+ self.fontmode = "1"
+ else:
+ self.fontmode = "L" # aliasing is okay for other modes
+ self.fill = False
+
+ def getfont(
+ self,
+ ) -> ImageFont.BaseImageFont:
+ """
+ Get the current default font.
+
+ To set the default font for this ImageDraw instance::
+
+ from PIL import ImageDraw, ImageFont
+ draw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
+ To set the default font for all future ImageDraw instances::
+
+ from PIL import ImageDraw, ImageFont
+ ImageDraw.ImageDraw.font = ImageFont.truetype("Tests/fonts/FreeMono.ttf")
+
+ If the current default font is ``None``,
+ it is initialized with ``ImageFont.load_default()``.
+
+ :returns: An image font."""
+ if not self.font:
+ # FIXME: should add a font repository
+ self.font = ImageFont.load_default()
+ return self.font
+
+ def _getfont(self, font_size: float | None) -> ImageFont.BaseImageFont:
+ if font_size is not None:
+ return ImageFont.load_default(font_size)
+ else:
+ return self.getfont()
+
+ def _getink(
+ self, ink: _Ink | None, fill: _Ink | None = None
+ ) -> tuple[int | None, int | None]:
+ result_ink = None
+ result_fill = None
+ if ink is None and fill is None:
+ if self.fill:
+ result_fill = self.ink
+ else:
+ result_ink = self.ink
+ else:
+ if ink is not None:
+ if isinstance(ink, str):
+ ink = ImageColor.getcolor(ink, self.mode)
+ if self.palette and isinstance(ink, tuple):
+ ink = self.palette.getcolor(ink, self._image)
+ result_ink = self.draw.draw_ink(ink)
+ if fill is not None:
+ if isinstance(fill, str):
+ fill = ImageColor.getcolor(fill, self.mode)
+ if self.palette and isinstance(fill, tuple):
+ fill = self.palette.getcolor(fill, self._image)
+ result_fill = self.draw.draw_ink(fill)
+ return result_ink, result_fill
+
+ def arc(
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw an arc."""
+ ink, fill = self._getink(fill)
+ if ink is not None and width != 0:
+ self.draw.draw_arc(xy, start, end, ink, width)
+
+ def bitmap(
+ self, xy: Sequence[int], bitmap: Image.Image, fill: _Ink | None = None
+ ) -> None:
+ """Draw a bitmap."""
+ bitmap.load()
+ ink, fill = self._getink(fill)
+ if ink is None:
+ ink = fill
+ if ink is not None:
+ self.draw.draw_bitmap(xy, bitmap.im, ink)
+
+ def chord(
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw a chord."""
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_chord(xy, start, end, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
+ self.draw.draw_chord(xy, start, end, ink, 0, width)
+
+ def ellipse(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw an ellipse."""
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_ellipse(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
+ self.draw.draw_ellipse(xy, ink, 0, width)
+
+ def circle(
+ self,
+ xy: Sequence[float],
+ radius: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw a circle given center coordinates and a radius."""
+ ellipse_xy = (xy[0] - radius, xy[1] - radius, xy[0] + radius, xy[1] + radius)
+ self.ellipse(ellipse_xy, fill, outline, width)
+
+ def line(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ width: int = 1,
+ joint: str | None = None,
+ ) -> None:
+ """Draw a line, or a connected sequence of line segments."""
+ ink = self._getink(fill)[0]
+ if ink is not None and width != 0:
+ self.draw.draw_lines(xy, ink, width)
+ if joint == "curve" and width > 4:
+ points: Sequence[Sequence[float]]
+ if isinstance(xy[0], (list, tuple)):
+ points = cast(Sequence[Sequence[float]], xy)
+ else:
+ points = [
+ cast(Sequence[float], tuple(xy[i : i + 2]))
+ for i in range(0, len(xy), 2)
+ ]
+ for i in range(1, len(points) - 1):
+ point = points[i]
+ angles = [
+ math.degrees(math.atan2(end[0] - start[0], start[1] - end[1]))
+ % 360
+ for start, end in (
+ (points[i - 1], point),
+ (point, points[i + 1]),
+ )
+ ]
+ if angles[0] == angles[1]:
+ # This is a straight line, so no joint is required
+ continue
+
+ def coord_at_angle(
+ coord: Sequence[float], angle: float
+ ) -> tuple[float, ...]:
+ x, y = coord
+ angle -= 90
+ distance = width / 2 - 1
+ return tuple(
+ p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
+ for p, p_d in (
+ (x, distance * math.cos(math.radians(angle))),
+ (y, distance * math.sin(math.radians(angle))),
+ )
+ )
+
+ flipped = (
+ angles[1] > angles[0] and angles[1] - 180 > angles[0]
+ ) or (angles[1] < angles[0] and angles[1] + 180 > angles[0])
+ coords = [
+ (point[0] - width / 2 + 1, point[1] - width / 2 + 1),
+ (point[0] + width / 2 - 1, point[1] + width / 2 - 1),
+ ]
+ if flipped:
+ start, end = (angles[1] + 90, angles[0] + 90)
+ else:
+ start, end = (angles[0] - 90, angles[1] - 90)
+ self.pieslice(coords, start - 90, end - 90, fill)
+
+ if width > 8:
+ # Cover potential gaps between the line and the joint
+ if flipped:
+ gap_coords = [
+ coord_at_angle(point, angles[0] + 90),
+ point,
+ coord_at_angle(point, angles[1] + 90),
+ ]
+ else:
+ gap_coords = [
+ coord_at_angle(point, angles[0] - 90),
+ point,
+ coord_at_angle(point, angles[1] - 90),
+ ]
+ self.line(gap_coords, fill, width=3)
+
+ def shape(
+ self,
+ shape: Image.core._Outline,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ ) -> None:
+ """(Experimental) Draw a shape."""
+ shape.close()
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_outline(shape, fill_ink, 1)
+ if ink is not None and ink != fill_ink:
+ self.draw.draw_outline(shape, ink, 0)
+
+ def pieslice(
+ self,
+ xy: Coords,
+ start: float,
+ end: float,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw a pieslice."""
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_pieslice(xy, start, end, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
+ self.draw.draw_pieslice(xy, start, end, ink, 0, width)
+
+ def point(self, xy: Coords, fill: _Ink | None = None) -> None:
+ """Draw one or more individual pixels."""
+ ink, fill = self._getink(fill)
+ if ink is not None:
+ self.draw.draw_points(xy, ink)
+
+ def polygon(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw a polygon."""
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_polygon(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
+ if width == 1:
+ self.draw.draw_polygon(xy, ink, 0, width)
+ elif self.im is not None:
+ # To avoid expanding the polygon outwards,
+ # use the fill as a mask
+ mask = Image.new("1", self.im.size)
+ mask_ink = self._getink(1)[0]
+ draw = Draw(mask)
+ draw.draw.draw_polygon(xy, mask_ink, 1)
+
+ self.draw.draw_polygon(xy, ink, 0, width * 2 - 1, mask.im)
+
+ def regular_polygon(
+ self,
+ bounding_circle: Sequence[Sequence[float] | float],
+ n_sides: int,
+ rotation: float = 0,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw a regular polygon."""
+ xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
+ self.polygon(xy, fill, outline, width)
+
+ def rectangle(
+ self,
+ xy: Coords,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ ) -> None:
+ """Draw a rectangle."""
+ ink, fill_ink = self._getink(outline, fill)
+ if fill_ink is not None:
+ self.draw.draw_rectangle(xy, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
+ self.draw.draw_rectangle(xy, ink, 0, width)
+
+ def rounded_rectangle(
+ self,
+ xy: Coords,
+ radius: float = 0,
+ fill: _Ink | None = None,
+ outline: _Ink | None = None,
+ width: int = 1,
+ *,
+ corners: tuple[bool, bool, bool, bool] | None = None,
+ ) -> None:
+ """Draw a rounded rectangle."""
+ if isinstance(xy[0], (list, tuple)):
+ (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy)
+ else:
+ x0, y0, x1, y1 = cast(Sequence[float], xy)
+ if x1 < x0:
+ msg = "x1 must be greater than or equal to x0"
+ raise ValueError(msg)
+ if y1 < y0:
+ msg = "y1 must be greater than or equal to y0"
+ raise ValueError(msg)
+ if corners is None:
+ corners = (True, True, True, True)
+
+ d = min(x1 - x0, y1 - y0, radius * 2)
+
+ x0 = round(x0)
+ y0 = round(y0)
+ x1 = round(x1)
+ y1 = round(y1)
+ full_x, full_y = False, False
+ if all(corners):
+ full_x = d >= x1 - x0 - 1
+ if full_x:
+ # The two left and two right corners are joined
+ d = x1 - x0
+ full_y = d >= y1 - y0 - 1
+ if full_y:
+ # The two top and two bottom corners are joined
+ d = y1 - y0
+ if full_x and full_y:
+ # If all corners are joined, that is a circle
+ return self.ellipse(xy, fill, outline, width)
+
+ if d == 0 or not any(corners):
+ # If the corners have no curve,
+ # or there are no corners,
+ # that is a rectangle
+ return self.rectangle(xy, fill, outline, width)
+
+ r = int(d // 2)
+ ink, fill_ink = self._getink(outline, fill)
+
+ def draw_corners(pieslice: bool) -> None:
+ parts: tuple[tuple[tuple[float, float, float, float], int, int], ...]
+ if full_x:
+ # Draw top and bottom halves
+ parts = (
+ ((x0, y0, x0 + d, y0 + d), 180, 360),
+ ((x0, y1 - d, x0 + d, y1), 0, 180),
+ )
+ elif full_y:
+ # Draw left and right halves
+ parts = (
+ ((x0, y0, x0 + d, y0 + d), 90, 270),
+ ((x1 - d, y0, x1, y0 + d), 270, 90),
+ )
+ else:
+ # Draw four separate corners
+ parts = tuple(
+ part
+ for i, part in enumerate(
+ (
+ ((x0, y0, x0 + d, y0 + d), 180, 270),
+ ((x1 - d, y0, x1, y0 + d), 270, 360),
+ ((x1 - d, y1 - d, x1, y1), 0, 90),
+ ((x0, y1 - d, x0 + d, y1), 90, 180),
+ )
+ )
+ if corners[i]
+ )
+ for part in parts:
+ if pieslice:
+ self.draw.draw_pieslice(*(part + (fill_ink, 1)))
+ else:
+ self.draw.draw_arc(*(part + (ink, width)))
+
+ if fill_ink is not None:
+ draw_corners(True)
+
+ if full_x:
+ self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1)
+ elif x1 - r - 1 >= x0 + r + 1:
+ self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1)
+ if not full_x and not full_y:
+ left = [x0, y0, x0 + r, y1]
+ if corners[0]:
+ left[1] += r + 1
+ if corners[3]:
+ left[3] -= r + 1
+ self.draw.draw_rectangle(left, fill_ink, 1)
+
+ right = [x1 - r, y0, x1, y1]
+ if corners[1]:
+ right[1] += r + 1
+ if corners[2]:
+ right[3] -= r + 1
+ self.draw.draw_rectangle(right, fill_ink, 1)
+ if ink is not None and ink != fill_ink and width != 0:
+ draw_corners(False)
+
+ if not full_x:
+ top = [x0, y0, x1, y0 + width - 1]
+ if corners[0]:
+ top[0] += r + 1
+ if corners[1]:
+ top[2] -= r + 1
+ self.draw.draw_rectangle(top, ink, 1)
+
+ bottom = [x0, y1 - width + 1, x1, y1]
+ if corners[3]:
+ bottom[0] += r + 1
+ if corners[2]:
+ bottom[2] -= r + 1
+ self.draw.draw_rectangle(bottom, ink, 1)
+ if not full_y:
+ left = [x0, y0, x0 + width - 1, y1]
+ if corners[0]:
+ left[1] += r + 1
+ if corners[3]:
+ left[3] -= r + 1
+ self.draw.draw_rectangle(left, ink, 1)
+
+ right = [x1 - width + 1, y0, x1, y1]
+ if corners[1]:
+ right[1] += r + 1
+ if corners[2]:
+ right[3] -= r + 1
+ self.draw.draw_rectangle(right, ink, 1)
+
+ def text(
+ self,
+ xy: tuple[float, float],
+ text: AnyStr | ImageText.Text[AnyStr],
+ fill: _Ink | None = None,
+ font: ImageFont.BaseImageFont | None = None,
+ anchor: str | None = None,
+ spacing: float = 4,
+ align: str = "left",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ stroke_fill: _Ink | None = None,
+ embedded_color: bool = False,
+ *args: Any,
+ **kwargs: Any,
+ ) -> None:
+ """Draw text."""
+ if isinstance(text, ImageText.Text):
+ image_text = text
+ else:
+ if font is None:
+ font = self._getfont(kwargs.get("font_size"))
+ image_text = ImageText.Text(
+ text, font, self.mode, spacing, direction, features, language
+ )
+ if embedded_color:
+ image_text.embed_color()
+ if stroke_width:
+ image_text.stroke(stroke_width, stroke_fill)
+
+ def getink(fill: _Ink | None) -> int:
+ ink, fill_ink = self._getink(fill)
+ if ink is None:
+ assert fill_ink is not None
+ return fill_ink
+ return ink
+
+ ink = getink(fill)
+ if ink is None:
+ return
+
+ stroke_ink = None
+ if image_text.stroke_width:
+ stroke_ink = (
+ getink(image_text.stroke_fill)
+ if image_text.stroke_fill is not None
+ else ink
+ )
+
+ for line in image_text._split(xy, anchor, align):
+
+ def draw_text(ink: int, stroke_width: float = 0) -> None:
+ mode = self.fontmode
+ if stroke_width == 0 and embedded_color:
+ mode = "RGBA"
+ x = int(line.x)
+ y = int(line.y)
+ start = (math.modf(line.x)[0], math.modf(line.y)[0])
+ if isinstance(image_text.font, ImageFont.FreeTypeFont):
+ mask, offset = image_text.font.getmask2(
+ line.text,
+ mode,
+ direction,
+ features,
+ language,
+ stroke_width,
+ line.anchor,
+ ink,
+ start,
+ stroke_filled=True,
+ *args,
+ **kwargs,
+ )
+ x += offset[0]
+ y += offset[1]
+ else:
+ try:
+ mask = image_text.font.getmask(
+ line.text,
+ mode,
+ direction,
+ features,
+ language,
+ stroke_width,
+ line.anchor,
+ ink,
+ start=start,
+ *args,
+ **kwargs,
+ )
+ except TypeError:
+ mask = image_text.font.getmask(line.text)
+ if mode == "RGBA":
+ # image_text.font.getmask2(mode="RGBA")
+ # returns color in RGB bands and mask in A
+ # extract mask and set text alpha
+ color, mask = mask, mask.getband(3)
+ ink_alpha = struct.pack("i", ink)[3]
+ color.fillband(3, ink_alpha)
+ if self.im is not None:
+ self.im.paste(
+ color, (x, y, x + mask.size[0], y + mask.size[1]), mask
+ )
+ else:
+ self.draw.draw_bitmap((x, y), mask, ink)
+
+ if stroke_ink is not None:
+ # Draw stroked text
+ draw_text(stroke_ink, image_text.stroke_width)
+
+ # Draw normal text
+ if ink != stroke_ink:
+ draw_text(ink)
+ else:
+ # Only draw normal text
+ draw_text(ink)
+
+ def multiline_text(
+ self,
+ xy: tuple[float, float],
+ text: AnyStr,
+ fill: _Ink | None = None,
+ font: ImageFont.BaseImageFont | None = None,
+ anchor: str | None = None,
+ spacing: float = 4,
+ align: str = "left",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ stroke_fill: _Ink | None = None,
+ embedded_color: bool = False,
+ *,
+ font_size: float | None = None,
+ ) -> None:
+ return self.text(
+ xy,
+ text,
+ fill,
+ font,
+ anchor,
+ spacing,
+ align,
+ direction,
+ features,
+ language,
+ stroke_width,
+ stroke_fill,
+ embedded_color,
+ font_size=font_size,
+ )
+
+ def textlength(
+ self,
+ text: AnyStr,
+ font: ImageFont.BaseImageFont | None = None,
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ embedded_color: bool = False,
+ *,
+ font_size: float | None = None,
+ ) -> float:
+ """Get the length of a given string, in pixels with 1/64 precision."""
+ if font is None:
+ font = self._getfont(font_size)
+ image_text = ImageText.Text(
+ text,
+ font,
+ self.mode,
+ direction=direction,
+ features=features,
+ language=language,
+ )
+ if embedded_color:
+ image_text.embed_color()
+ return image_text.get_length()
+
+ def textbbox(
+ self,
+ xy: tuple[float, float],
+ text: AnyStr,
+ font: ImageFont.BaseImageFont | None = None,
+ anchor: str | None = None,
+ spacing: float = 4,
+ align: str = "left",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ embedded_color: bool = False,
+ *,
+ font_size: float | None = None,
+ ) -> tuple[float, float, float, float]:
+ """Get the bounding box of a given string, in pixels."""
+ if font is None:
+ font = self._getfont(font_size)
+ image_text = ImageText.Text(
+ text, font, self.mode, spacing, direction, features, language
+ )
+ if embedded_color:
+ image_text.embed_color()
+ if stroke_width:
+ image_text.stroke(stroke_width)
+ return image_text.get_bbox(xy, anchor, align)
+
+ def multiline_textbbox(
+ self,
+ xy: tuple[float, float],
+ text: AnyStr,
+ font: ImageFont.BaseImageFont | None = None,
+ anchor: str | None = None,
+ spacing: float = 4,
+ align: str = "left",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ embedded_color: bool = False,
+ *,
+ font_size: float | None = None,
+ ) -> tuple[float, float, float, float]:
+ return self.textbbox(
+ xy,
+ text,
+ font,
+ anchor,
+ spacing,
+ align,
+ direction,
+ features,
+ language,
+ stroke_width,
+ embedded_color,
+ font_size=font_size,
+ )
+
+
+def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
+ """
+ A simple 2D drawing interface for PIL images.
+
+ :param im: The image to draw in.
+ :param mode: Optional mode to use for color values. For RGB
+ images, this argument can be RGB or RGBA (to blend the
+ drawing into the image). For all other modes, this argument
+ must be the same as the image mode. If omitted, the mode
+ defaults to the mode of the image.
+ """
+ try:
+ return getattr(im, "getdraw")(mode)
+ except AttributeError:
+ return ImageDraw(im, mode)
+
+
+def getdraw(im: Image.Image | None = None) -> tuple[ImageDraw2.Draw | None, ModuleType]:
+ """
+ :param im: The image to draw in.
+ :returns: A (drawing context, drawing resource factory) tuple.
+ """
+ from . import ImageDraw2
+
+ draw = ImageDraw2.Draw(im) if im is not None else None
+ return draw, ImageDraw2
+
+
+def floodfill(
+ image: Image.Image,
+ xy: tuple[int, int],
+ value: float | tuple[int, ...],
+ border: float | tuple[int, ...] | None = None,
+ thresh: float = 0,
+) -> None:
+ """
+ .. warning:: This method is experimental.
+
+ Fills a bounded region with a given color.
+
+ :param image: Target image.
+ :param xy: Seed position (a 2-item coordinate tuple). See
+ :ref:`coordinate-system`.
+ :param value: Fill color.
+ :param border: Optional border value. If given, the region consists of
+ pixels with a color different from the border color. If not given,
+ the region consists of pixels having the same color as the seed
+ pixel.
+ :param thresh: Optional threshold value which specifies a maximum
+ tolerable difference of a pixel value from the 'background' in
+ order for it to be replaced. Useful for filling regions of
+ non-homogeneous, but similar, colors.
+ """
+ # based on an implementation by Eric S. Raymond
+ # amended by yo1995 @20180806
+ pixel = image.load()
+ assert pixel is not None
+ x, y = xy
+ try:
+ background = pixel[x, y]
+ if _color_diff(value, background) <= thresh:
+ return # seed point already has fill color
+ pixel[x, y] = value
+ except (ValueError, IndexError):
+ return # seed point outside image
+ edge = {(x, y)}
+ # use a set to keep record of current and previous edge pixels
+ # to reduce memory consumption
+ full_edge = set()
+ while edge:
+ new_edge = set()
+ for x, y in edge: # 4 adjacent method
+ for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)):
+ # If already processed, or if a coordinate is negative, skip
+ if (s, t) in full_edge or s < 0 or t < 0:
+ continue
+ try:
+ p = pixel[s, t]
+ except (ValueError, IndexError):
+ pass
+ else:
+ full_edge.add((s, t))
+ if border is None:
+ fill = _color_diff(p, background) <= thresh
+ else:
+ fill = p not in (value, border)
+ if fill:
+ pixel[s, t] = value
+ new_edge.add((s, t))
+ full_edge = edge # discard pixels processed
+ edge = new_edge
+
+
+def _compute_regular_polygon_vertices(
+ bounding_circle: Sequence[Sequence[float] | float], n_sides: int, rotation: float
+) -> list[tuple[float, float]]:
+ """
+ Generate a list of vertices for a 2D regular polygon.
+
+ :param bounding_circle: The bounding circle is a sequence defined
+ by a point and radius. The polygon is inscribed in this circle.
+ (e.g. ``bounding_circle=(x, y, r)`` or ``((x, y), r)``)
+ :param n_sides: Number of sides
+ (e.g. ``n_sides=3`` for a triangle, ``6`` for a hexagon)
+ :param rotation: Apply an arbitrary rotation to the polygon
+ (e.g. ``rotation=90``, applies a 90 degree rotation)
+ :return: List of regular polygon vertices
+ (e.g. ``[(25, 50), (50, 50), (50, 25), (25, 25)]``)
+
+ How are the vertices computed?
+ 1. Compute the following variables
+ - theta: Angle between the apothem & the nearest polygon vertex
+ - side_length: Length of each polygon edge
+ - centroid: Center of bounding circle (1st, 2nd elements of bounding_circle)
+ - polygon_radius: Polygon radius (last element of bounding_circle)
+ - angles: Location of each polygon vertex in polar grid
+ (e.g. A square with 0 degree rotation => [225.0, 315.0, 45.0, 135.0])
+
+ 2. For each angle in angles, get the polygon vertex at that angle
+ The vertex is computed using the equation below.
+ X= xcos(φ) + ysin(φ)
+ Y= −xsin(φ) + ycos(φ)
+
+ Note:
+ φ = angle in degrees
+ x = 0
+ y = polygon_radius
+
+ The formula above assumes rotation around the origin.
+ In our case, we are rotating around the centroid.
+ To account for this, we use the formula below
+ X = xcos(φ) + ysin(φ) + centroid_x
+ Y = −xsin(φ) + ycos(φ) + centroid_y
+ """
+ # 1. Error Handling
+ # 1.1 Check `n_sides` has an appropriate value
+ if not isinstance(n_sides, int):
+ msg = "n_sides should be an int" # type: ignore[unreachable]
+ raise TypeError(msg)
+ if n_sides < 3:
+ msg = "n_sides should be an int > 2"
+ raise ValueError(msg)
+
+ # 1.2 Check `bounding_circle` has an appropriate value
+ if not isinstance(bounding_circle, (list, tuple)):
+ msg = "bounding_circle should be a sequence"
+ raise TypeError(msg)
+
+ if len(bounding_circle) == 3:
+ if not all(isinstance(i, (int, float)) for i in bounding_circle):
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
+
+ *centroid, polygon_radius = cast(list[float], list(bounding_circle))
+ elif len(bounding_circle) == 2 and isinstance(bounding_circle[0], (list, tuple)):
+ if not all(
+ isinstance(i, (int, float)) for i in bounding_circle[0]
+ ) or not isinstance(bounding_circle[1], (int, float)):
+ msg = "bounding_circle should only contain numeric data"
+ raise ValueError(msg)
+
+ if len(bounding_circle[0]) != 2:
+ msg = "bounding_circle centre should contain 2D coordinates (e.g. (x, y))"
+ raise ValueError(msg)
+
+ centroid = cast(list[float], list(bounding_circle[0]))
+ polygon_radius = cast(float, bounding_circle[1])
+ else:
+ msg = (
+ "bounding_circle should contain 2D coordinates "
+ "and a radius (e.g. (x, y, r) or ((x, y), r) )"
+ )
+ raise ValueError(msg)
+
+ if polygon_radius <= 0:
+ msg = "bounding_circle radius should be > 0"
+ raise ValueError(msg)
+
+ # 1.3 Check `rotation` has an appropriate value
+ if not isinstance(rotation, (int, float)):
+ msg = "rotation should be an int or float" # type: ignore[unreachable]
+ raise ValueError(msg)
+
+ # 2. Define Helper Functions
+ def _apply_rotation(point: list[float], degrees: float) -> tuple[float, float]:
+ return (
+ round(
+ point[0] * math.cos(math.radians(360 - degrees))
+ - point[1] * math.sin(math.radians(360 - degrees))
+ + centroid[0],
+ 2,
+ ),
+ round(
+ point[1] * math.cos(math.radians(360 - degrees))
+ + point[0] * math.sin(math.radians(360 - degrees))
+ + centroid[1],
+ 2,
+ ),
+ )
+
+ def _compute_polygon_vertex(angle: float) -> tuple[float, float]:
+ start_point = [polygon_radius, 0]
+ return _apply_rotation(start_point, angle)
+
+ def _get_angles(n_sides: int, rotation: float) -> list[float]:
+ angles = []
+ degrees = 360 / n_sides
+ # Start with the bottom left polygon vertex
+ current_angle = (270 - 0.5 * degrees) + rotation
+ for _ in range(n_sides):
+ angles.append(current_angle)
+ current_angle += degrees
+ if current_angle > 360:
+ current_angle -= 360
+ return angles
+
+ # 3. Variable Declarations
+ angles = _get_angles(n_sides, rotation)
+
+ # 4. Compute Vertices
+ return [_compute_polygon_vertex(angle) for angle in angles]
+
+
+def _color_diff(
+ color1: float | tuple[int, ...], color2: float | tuple[int, ...]
+) -> float:
+ """
+ Uses 1-norm distance to calculate difference between two values.
+ """
+ first = color1 if isinstance(color1, tuple) else (color1,)
+ second = color2 if isinstance(color2, tuple) else (color2,)
+
+ return sum(abs(first[i] - second[i]) for i in range(len(second)))
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageDraw2.py b/venv/lib/python3.11/site-packages/PIL/ImageDraw2.py
new file mode 100644
index 000000000..2c9e39b2c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageDraw2.py
@@ -0,0 +1,244 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# WCK-style drawing interface operations
+#
+# History:
+# 2003-12-07 fl created
+# 2005-05-15 fl updated; added to PIL as ImageDraw2
+# 2005-05-15 fl added text support
+# 2005-05-20 fl added arc/chord/pieslice support
+#
+# Copyright (c) 2003-2005 by Secret Labs AB
+# Copyright (c) 2003-2005 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+
+"""
+(Experimental) WCK-style drawing interface operations
+
+.. seealso:: :py:mod:`PIL.ImageDraw`
+"""
+
+from __future__ import annotations
+
+from typing import Any, AnyStr, BinaryIO
+
+from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
+from ._typing import Coords, StrOrBytesPath
+
+
+class Pen:
+ """Stores an outline color and width."""
+
+ def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None:
+ self.color = ImageColor.getrgb(color)
+ self.width = width
+
+
+class Brush:
+ """Stores a fill color"""
+
+ def __init__(self, color: str, opacity: int = 255) -> None:
+ self.color = ImageColor.getrgb(color)
+
+
+class Font:
+ """Stores a TrueType font and color"""
+
+ def __init__(
+ self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12
+ ) -> None:
+ # FIXME: add support for bitmap fonts
+ self.color = ImageColor.getrgb(color)
+ self.font = ImageFont.truetype(file, size)
+
+
+class Draw:
+ """
+ (Experimental) WCK-style drawing interface
+ """
+
+ def __init__(
+ self,
+ image: Image.Image | str,
+ size: tuple[int, int] | list[int] | None = None,
+ color: float | tuple[float, ...] | str | None = None,
+ ) -> None:
+ if isinstance(image, str):
+ if size is None:
+ msg = "If image argument is mode string, size must be a list or tuple"
+ raise ValueError(msg)
+ image = Image.new(image, size, color)
+ self.draw = ImageDraw.Draw(image)
+ self.image = image
+ self.transform: tuple[float, float, float, float, float, float] | None = None
+
+ def flush(self) -> Image.Image:
+ return self.image
+
+ def render(
+ self,
+ op: str,
+ xy: Coords,
+ pen: Pen | Brush | None,
+ brush: Brush | Pen | None = None,
+ **kwargs: Any,
+ ) -> None:
+ # handle color arguments
+ outline = fill = None
+ width = 1
+ if isinstance(pen, Pen):
+ outline = pen.color
+ width = pen.width
+ elif isinstance(brush, Pen):
+ outline = brush.color
+ width = brush.width
+ if isinstance(brush, Brush):
+ fill = brush.color
+ elif isinstance(pen, Brush):
+ fill = pen.color
+ # handle transformation
+ if self.transform:
+ path = ImagePath.Path(xy)
+ path.transform(self.transform)
+ xy = path
+ # render the item
+ if op in ("arc", "line"):
+ kwargs.setdefault("fill", outline)
+ else:
+ kwargs.setdefault("fill", fill)
+ kwargs.setdefault("outline", outline)
+ if op == "line":
+ kwargs.setdefault("width", width)
+ getattr(self.draw, op)(xy, **kwargs)
+
+ def settransform(self, offset: tuple[float, float]) -> None:
+ """Sets a transformation offset."""
+ xoffset, yoffset = offset
+ self.transform = (1, 0, xoffset, 0, 1, yoffset)
+
+ def arc(
+ self,
+ xy: Coords,
+ pen: Pen | Brush | None,
+ start: float,
+ end: float,
+ *options: Any,
+ ) -> None:
+ """
+ Draws an arc (a portion of a circle outline) between the start and end
+ angles, inside the given bounding box.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.arc`
+ """
+ self.render("arc", xy, pen, *options, start=start, end=end)
+
+ def chord(
+ self,
+ xy: Coords,
+ pen: Pen | Brush | None,
+ start: float,
+ end: float,
+ *options: Any,
+ ) -> None:
+ """
+ Same as :py:meth:`~PIL.ImageDraw2.Draw.arc`, but connects the end points
+ with a straight line.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.chord`
+ """
+ self.render("chord", xy, pen, *options, start=start, end=end)
+
+ def ellipse(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
+ """
+ Draws an ellipse inside the given bounding box.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.ellipse`
+ """
+ self.render("ellipse", xy, pen, *options)
+
+ def line(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
+ """
+ Draws a line between the coordinates in the ``xy`` list.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.line`
+ """
+ self.render("line", xy, pen, *options)
+
+ def pieslice(
+ self,
+ xy: Coords,
+ pen: Pen | Brush | None,
+ start: float,
+ end: float,
+ *options: Any,
+ ) -> None:
+ """
+ Same as arc, but also draws straight lines between the end points and the
+ center of the bounding box.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.pieslice`
+ """
+ self.render("pieslice", xy, pen, *options, start=start, end=end)
+
+ def polygon(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
+ """
+ Draws a polygon.
+
+ The polygon outline consists of straight lines between the given
+ coordinates, plus a straight line between the last and the first
+ coordinate.
+
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.polygon`
+ """
+ self.render("polygon", xy, pen, *options)
+
+ def rectangle(self, xy: Coords, pen: Pen | Brush | None, *options: Any) -> None:
+ """
+ Draws a rectangle.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.rectangle`
+ """
+ self.render("rectangle", xy, pen, *options)
+
+ def text(self, xy: tuple[float, float], text: AnyStr, font: Font) -> None:
+ """
+ Draws the string at the given position.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.text`
+ """
+ if self.transform:
+ path = ImagePath.Path(xy)
+ path.transform(self.transform)
+ xy = path
+ self.draw.text(xy, text, font=font.font, fill=font.color)
+
+ def textbbox(
+ self, xy: tuple[float, float], text: AnyStr, font: Font
+ ) -> tuple[float, float, float, float]:
+ """
+ Returns bounding box (in pixels) of given text.
+
+ :return: ``(left, top, right, bottom)`` bounding box
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textbbox`
+ """
+ if self.transform:
+ path = ImagePath.Path(xy)
+ path.transform(self.transform)
+ xy = path
+ return self.draw.textbbox(xy, text, font=font.font)
+
+ def textlength(self, text: AnyStr, font: Font) -> float:
+ """
+ Returns length (in pixels) of given text.
+ This is the amount by which following text should be offset.
+
+ .. seealso:: :py:meth:`PIL.ImageDraw.ImageDraw.textlength`
+ """
+ return self.draw.textlength(text, font=font.font)
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageEnhance.py b/venv/lib/python3.11/site-packages/PIL/ImageEnhance.py
new file mode 100644
index 000000000..0e7e6dd8a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageEnhance.py
@@ -0,0 +1,113 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# image enhancement classes
+#
+# For a background, see "Image Processing By Interpolation and
+# Extrapolation", Paul Haeberli and Douglas Voorhies. Available
+# at http://www.graficaobscura.com/interp/index.html
+#
+# History:
+# 1996-03-23 fl Created
+# 2009-06-16 fl Fixed mean calculation
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1996.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from . import Image, ImageFilter, ImageStat
+
+
+class _Enhance:
+ image: Image.Image
+ degenerate: Image.Image
+
+ def enhance(self, factor: float) -> Image.Image:
+ """
+ Returns an enhanced image.
+
+ :param factor: A floating point value controlling the enhancement.
+ Factor 1.0 always returns a copy of the original image,
+ lower factors mean less color (brightness, contrast,
+ etc), and higher values more. There are no restrictions
+ on this value.
+ :rtype: :py:class:`~PIL.Image.Image`
+ """
+ return Image.blend(self.degenerate, self.image, factor)
+
+
+class Color(_Enhance):
+ """Adjust image color balance.
+
+ This class can be used to adjust the colour balance of an image, in
+ a manner similar to the controls on a colour TV set. An enhancement
+ factor of 0.0 gives a black and white image. A factor of 1.0 gives
+ the original image.
+ """
+
+ def __init__(self, image: Image.Image) -> None:
+ self.image = image
+ self.intermediate_mode = "L"
+ if "A" in image.getbands():
+ self.intermediate_mode = "LA"
+
+ if self.intermediate_mode != image.mode:
+ image = image.convert(self.intermediate_mode).convert(image.mode)
+ self.degenerate = image
+
+
+class Contrast(_Enhance):
+ """Adjust image contrast.
+
+ This class can be used to control the contrast of an image, similar
+ to the contrast control on a TV set. An enhancement factor of 0.0
+ gives a solid gray image. A factor of 1.0 gives the original image.
+ """
+
+ def __init__(self, image: Image.Image) -> None:
+ self.image = image
+ if image.mode != "L":
+ image = image.convert("L")
+ mean = int(ImageStat.Stat(image).mean[0] + 0.5)
+ self.degenerate = Image.new("L", image.size, mean)
+ if self.degenerate.mode != self.image.mode:
+ self.degenerate = self.degenerate.convert(self.image.mode)
+
+ if "A" in self.image.getbands():
+ self.degenerate.putalpha(self.image.getchannel("A"))
+
+
+class Brightness(_Enhance):
+ """Adjust image brightness.
+
+ This class can be used to control the brightness of an image. An
+ enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the
+ original image.
+ """
+
+ def __init__(self, image: Image.Image) -> None:
+ self.image = image
+ self.degenerate = Image.new(image.mode, image.size, 0)
+
+ if "A" in image.getbands():
+ self.degenerate.putalpha(image.getchannel("A"))
+
+
+class Sharpness(_Enhance):
+ """Adjust image sharpness.
+
+ This class can be used to adjust the sharpness of an image. An
+ enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the
+ original image, and a factor of 2.0 gives a sharpened image.
+ """
+
+ def __init__(self, image: Image.Image) -> None:
+ self.image = image
+ self.degenerate = image.filter(ImageFilter.SMOOTH)
+
+ if "A" in image.getbands():
+ self.degenerate.putalpha(image.getchannel("A"))
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageFile.py b/venv/lib/python3.11/site-packages/PIL/ImageFile.py
new file mode 100644
index 000000000..2d69225b5
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageFile.py
@@ -0,0 +1,938 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# base class for image file handlers
+#
+# history:
+# 1995-09-09 fl Created
+# 1996-03-11 fl Fixed load mechanism.
+# 1996-04-15 fl Added pcx/xbm decoders.
+# 1996-04-30 fl Added encoders.
+# 1996-12-14 fl Added load helpers
+# 1997-01-11 fl Use encode_to_file where possible
+# 1997-08-27 fl Flush output in _save
+# 1998-03-05 fl Use memory mapping for some modes
+# 1999-02-04 fl Use memory mapping also for "I;16" and "I;16B"
+# 1999-05-31 fl Added image parser
+# 2000-10-12 fl Set readonly flag on memory-mapped images
+# 2002-03-20 fl Use better messages for common decoder errors
+# 2003-04-21 fl Fall back on mmap/map_buffer if map is not available
+# 2003-10-30 fl Added StubImageFile class
+# 2004-02-25 fl Made incremental parser more robust
+#
+# Copyright (c) 1997-2004 by Secret Labs AB
+# Copyright (c) 1995-2004 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import abc
+import io
+import itertools
+import logging
+import os
+import struct
+from typing import IO, Any, NamedTuple, cast
+
+from . import ExifTags, Image
+from ._util import DeferredError, is_path
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from ._typing import StrOrBytesPath
+
+logger = logging.getLogger(__name__)
+
+MAXBLOCK = 65536
+"""
+By default, Pillow processes image data in blocks. This helps to prevent excessive use
+of resources. Codecs may disable this behaviour with ``_pulls_fd`` or ``_pushes_fd``.
+
+When reading an image, this is the number of bytes to read at once.
+
+When writing an image, this is the number of bytes to write at once.
+If the image width times 4 is greater, then that will be used instead.
+Plugins may also set a greater number.
+
+User code may set this to another number.
+"""
+
+SAFEBLOCK = 1024 * 1024
+
+LOAD_TRUNCATED_IMAGES = False
+"""Whether or not to load truncated image files. User code may change this."""
+
+ERRORS = {
+ -1: "image buffer overrun error",
+ -2: "decoding error",
+ -3: "unknown error",
+ -8: "bad configuration",
+ -9: "out of memory error",
+}
+"""
+Dict of known error codes returned from :meth:`.PyDecoder.decode`,
+:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and
+:meth:`.PyEncoder.encode_to_file`.
+"""
+
+
+#
+# --------------------------------------------------------------------
+# Helpers
+
+
+def _get_oserror(error: int, *, encoder: bool) -> OSError:
+ try:
+ msg = Image.core.getcodecstatus(error)
+ except AttributeError:
+ msg = ERRORS.get(error)
+ if not msg:
+ msg = f"{'encoder' if encoder else 'decoder'} error {error}"
+ msg += f" when {'writing' if encoder else 'reading'} image file"
+ return OSError(msg)
+
+
+def _tilesort(t: _Tile) -> int:
+ # sort on offset
+ return t[2]
+
+
+class _Tile(NamedTuple):
+ codec_name: str
+ extents: tuple[int, int, int, int] | None
+ offset: int = 0
+ args: tuple[Any, ...] | str | None = None
+
+
+#
+# --------------------------------------------------------------------
+# ImageFile base class
+
+
+class ImageFile(Image.Image):
+ """Base class for image file format handlers."""
+
+ def __init__(
+ self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None
+ ) -> None:
+ super().__init__()
+
+ self._min_frame = 0
+
+ self.custom_mimetype: str | None = None
+
+ self.tile: list[_Tile] = []
+ """ A list of tile descriptors """
+
+ self.readonly = 1 # until we know better
+
+ self.decoderconfig: tuple[Any, ...] = ()
+ self.decodermaxblock = MAXBLOCK
+
+ self.fp: IO[bytes] | None
+ self._fp: IO[bytes] | DeferredError
+ if is_path(fp):
+ # filename
+ self.fp = open(fp, "rb")
+ self.filename = os.fspath(fp)
+ self._exclusive_fp = True
+ else:
+ # stream
+ self.fp = cast(IO[bytes], fp)
+ self.filename = filename if filename is not None else ""
+ # can be overridden
+ self._exclusive_fp = False
+
+ try:
+ try:
+ self._open()
+
+ if isinstance(self, StubImageFile):
+ if loader := self._load():
+ loader.open(self)
+ except (
+ IndexError, # end of data
+ TypeError, # end of data (ord)
+ KeyError, # unsupported mode
+ EOFError, # got header but not the first frame
+ struct.error,
+ ) as v:
+ raise SyntaxError(v) from v
+
+ if not self.mode or self.size[0] <= 0 or self.size[1] <= 0:
+ msg = "not identified by this driver"
+ raise SyntaxError(msg)
+ except BaseException:
+ # close the file only if we have opened it this constructor
+ if self._exclusive_fp:
+ self.fp.close()
+ raise
+
+ def _open(self) -> None:
+ pass
+
+ # Context manager support
+ def __enter__(self) -> ImageFile:
+ return self
+
+ def _close_fp(self) -> None:
+ if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError):
+ if self._fp != self.fp:
+ self._fp.close()
+ self._fp = DeferredError(ValueError("Operation on closed image"))
+ if self.fp:
+ self.fp.close()
+
+ def __exit__(self, *args: object) -> None:
+ if getattr(self, "_exclusive_fp", False):
+ self._close_fp()
+ self.fp = None
+
+ def close(self) -> None:
+ """
+ Closes the file pointer, if possible.
+
+ This operation will destroy the image core and release its memory.
+ The image data will be unusable afterward.
+
+ This function is required to close images that have multiple frames or
+ have not had their file read and closed by the
+ :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for
+ more information.
+ """
+ try:
+ self._close_fp()
+ self.fp = None
+ except Exception as msg:
+ logger.debug("Error closing: %s", msg)
+
+ super().close()
+
+ def get_child_images(self) -> list[ImageFile]:
+ child_images = []
+ exif = self.getexif()
+ ifds = []
+ if ExifTags.Base.SubIFDs in exif:
+ subifd_offsets = exif[ExifTags.Base.SubIFDs]
+ if subifd_offsets:
+ if not isinstance(subifd_offsets, tuple):
+ subifd_offsets = (subifd_offsets,)
+ ifds = [
+ (exif._get_ifd_dict(subifd_offset), subifd_offset)
+ for subifd_offset in subifd_offsets
+ ]
+ ifd1 = exif.get_ifd(ExifTags.IFD.IFD1)
+ if ifd1 and ifd1.get(ExifTags.Base.JpegIFOffset):
+ assert exif._info is not None
+ ifds.append((ifd1, exif._info.next))
+
+ offset = None
+ for ifd, ifd_offset in ifds:
+ assert self.fp is not None
+ current_offset = self.fp.tell()
+ if offset is None:
+ offset = current_offset
+
+ fp = self.fp
+ if ifd is not None:
+ thumbnail_offset = ifd.get(ExifTags.Base.JpegIFOffset)
+ if thumbnail_offset is not None:
+ thumbnail_offset += getattr(self, "_exif_offset", 0)
+ self.fp.seek(thumbnail_offset)
+
+ length = ifd.get(ExifTags.Base.JpegIFByteCount)
+ assert isinstance(length, int)
+ data = self.fp.read(length)
+ fp = io.BytesIO(data)
+
+ with Image.open(fp) as im:
+ from . import TiffImagePlugin
+
+ if thumbnail_offset is None and isinstance(
+ im, TiffImagePlugin.TiffImageFile
+ ):
+ im._frame_pos = [ifd_offset]
+ im._seek(0)
+ im.load()
+ child_images.append(im)
+
+ if offset is not None:
+ assert self.fp is not None
+ self.fp.seek(offset)
+ return child_images
+
+ def get_format_mimetype(self) -> str | None:
+ if self.custom_mimetype:
+ return self.custom_mimetype
+ if self.format is not None:
+ return Image.MIME.get(self.format.upper())
+ return None
+
+ def __getstate__(self) -> list[Any]:
+ return super().__getstate__() + [self.filename]
+
+ def __setstate__(self, state: list[Any]) -> None:
+ self.tile = []
+ if len(state) > 5:
+ self.filename = state[5]
+ super().__setstate__(state)
+
+ def verify(self) -> None:
+ """Check file integrity"""
+
+ # raise exception if something's wrong. must be called
+ # directly after open, and closes file when finished.
+ if self._exclusive_fp and self.fp:
+ self.fp.close()
+ self.fp = None
+
+ def load(self) -> Image.core.PixelAccess | None:
+ """Load image data based on tile list"""
+
+ if not self.tile and self._im is None:
+ msg = "cannot load this image"
+ raise OSError(msg)
+
+ pixel = Image.Image.load(self)
+ if not self.tile:
+ return pixel
+
+ self.map: mmap.mmap | None = None
+ use_mmap = self.filename and len(self.tile) == 1
+
+ assert self.fp is not None
+ readonly = 0
+
+ # look for read/seek overrides
+ if hasattr(self, "load_read"):
+ read = self.load_read
+ # don't use mmap if there are custom read/seek functions
+ use_mmap = False
+ else:
+ read = self.fp.read
+
+ if hasattr(self, "load_seek"):
+ seek = self.load_seek
+ use_mmap = False
+ else:
+ seek = self.fp.seek
+
+ if use_mmap:
+ # try memory mapping
+ decoder_name, extents, offset, args = self.tile[0]
+ if isinstance(args, str):
+ args = (args, 0, 1)
+ if (
+ decoder_name == "raw"
+ and isinstance(args, tuple)
+ and len(args) >= 3
+ and args[0] == self.mode
+ and args[0] in Image._MAPMODES
+ ):
+ if offset < 0:
+ msg = "Tile offset cannot be negative"
+ raise ValueError(msg)
+ try:
+ # use mmap, if possible
+ import mmap
+
+ with open(self.filename) as fp:
+ self.map = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
+ if offset + self.size[1] * args[1] > self.map.size():
+ msg = "buffer is not large enough"
+ raise OSError(msg)
+ self.im = Image.core.map_buffer(
+ self.map, self.size, decoder_name, offset, args
+ )
+ readonly = 1
+ # After trashing self.im,
+ # we might need to reload the palette data.
+ if self.palette:
+ self.palette.dirty = 1
+ except (AttributeError, OSError, ImportError):
+ self.map = None
+
+ self.load_prepare()
+ err_code = -3 # initialize to unknown error
+ if not self.map:
+ # sort tiles in file order
+ self.tile.sort(key=_tilesort)
+
+ # FIXME: This is a hack to handle TIFF's JpegTables tag.
+ prefix = getattr(self, "tile_prefix", b"")
+
+ # Remove consecutive duplicates that only differ by their offset
+ self.tile = [
+ list(tiles)[-1]
+ for _, tiles in itertools.groupby(
+ self.tile, lambda tile: (tile[0], tile[1], tile[3])
+ )
+ ]
+ for i, (decoder_name, extents, offset, args) in enumerate(self.tile):
+ seek(offset)
+ decoder = Image._getdecoder(
+ self.mode, decoder_name, args, self.decoderconfig
+ )
+ try:
+ decoder.setimage(self.im, extents)
+ if decoder.pulls_fd:
+ decoder.setfd(self.fp)
+ err_code = decoder.decode(b"")[1]
+ else:
+ b = prefix
+ while True:
+ read_bytes = self.decodermaxblock
+ if i + 1 < len(self.tile):
+ next_offset = self.tile[i + 1].offset
+ if next_offset > offset:
+ read_bytes = next_offset - offset
+ try:
+ s = read(read_bytes)
+ except (IndexError, struct.error) as e:
+ # truncated png/gif
+ if LOAD_TRUNCATED_IMAGES:
+ break
+ else:
+ msg = "image file is truncated"
+ raise OSError(msg) from e
+
+ if not s: # truncated jpeg
+ if LOAD_TRUNCATED_IMAGES:
+ break
+ else:
+ msg = (
+ "image file is truncated "
+ f"({len(b)} bytes not processed)"
+ )
+ raise OSError(msg)
+
+ b = b + s
+ n, err_code = decoder.decode(b)
+ if n < 0:
+ break
+ b = b[n:]
+ finally:
+ # Need to cleanup here to prevent leaks
+ decoder.cleanup()
+
+ self.tile = []
+ self.readonly = readonly
+
+ self.load_end()
+
+ if self._exclusive_fp and self._close_exclusive_fp_after_loading:
+ self.fp.close()
+ self.fp = None
+
+ if not self.map and not LOAD_TRUNCATED_IMAGES and err_code < 0:
+ # still raised if decoder fails to return anything
+ raise _get_oserror(err_code, encoder=False)
+
+ return Image.Image.load(self)
+
+ def load_prepare(self) -> None:
+ # create image memory if necessary
+ if self._im is None:
+ self.im = Image.core.new(self.mode, self.size)
+ # create palette (optional)
+ if self.mode == "P":
+ Image.Image.load(self)
+
+ def load_end(self) -> None:
+ # may be overridden
+ pass
+
+ # may be defined for contained formats
+ # def load_seek(self, pos: int) -> None:
+ # pass
+
+ # may be defined for blocked formats (e.g. PNG)
+ # def load_read(self, read_bytes: int) -> bytes:
+ # pass
+
+ def _seek_check(self, frame: int) -> bool:
+ if (
+ frame < self._min_frame
+ # Only check upper limit on frames if additional seek operations
+ # are not required to do so
+ or (
+ not (hasattr(self, "_n_frames") and self._n_frames is None)
+ and frame >= getattr(self, "n_frames") + self._min_frame
+ )
+ ):
+ msg = "attempt to seek outside sequence"
+ raise EOFError(msg)
+
+ return self.tell() != frame
+
+
+class StubHandler(abc.ABC):
+ def open(self, im: StubImageFile) -> None:
+ pass
+
+ @abc.abstractmethod
+ def load(self, im: StubImageFile) -> Image.Image:
+ pass
+
+
+class StubImageFile(ImageFile, metaclass=abc.ABCMeta):
+ """
+ Base class for stub image loaders.
+
+ A stub loader is an image loader that can identify files of a
+ certain format, but relies on external code to load the file.
+ """
+
+ @abc.abstractmethod
+ def _open(self) -> None:
+ pass
+
+ def load(self) -> Image.core.PixelAccess | None:
+ loader = self._load()
+ if loader is None:
+ msg = f"cannot find loader for this {self.format} file"
+ raise OSError(msg)
+ image = loader.load(self)
+ assert image is not None
+ # become the other object (!)
+ self.__class__ = image.__class__ # type: ignore[assignment]
+ self.__dict__ = image.__dict__
+ return image.load()
+
+ @abc.abstractmethod
+ def _load(self) -> StubHandler | None:
+ """(Hook) Find actual image loader."""
+ pass
+
+
+class Parser:
+ """
+ Incremental image parser. This class implements the standard
+ feed/close consumer interface.
+ """
+
+ incremental = None
+ image: Image.Image | None = None
+ data: bytes | None = None
+ decoder: Image.core.ImagingDecoder | PyDecoder | None = None
+ offset = 0
+ finished = 0
+
+ def reset(self) -> None:
+ """
+ (Consumer) Reset the parser. Note that you can only call this
+ method immediately after you've created a parser; parser
+ instances cannot be reused.
+ """
+ assert self.data is None, "cannot reuse parsers"
+
+ def feed(self, data: bytes) -> None:
+ """
+ (Consumer) Feed data to the parser.
+
+ :param data: A string buffer.
+ :exception OSError: If the parser failed to parse the image file.
+ """
+ # collect data
+
+ if self.finished:
+ return
+
+ if self.data is None:
+ self.data = data
+ else:
+ self.data = self.data + data
+
+ # parse what we have
+ if self.decoder:
+ if self.offset > 0:
+ # skip header
+ skip = min(len(self.data), self.offset)
+ self.data = self.data[skip:]
+ self.offset = self.offset - skip
+ if self.offset > 0 or not self.data:
+ return
+
+ n, e = self.decoder.decode(self.data)
+
+ if n < 0:
+ # end of stream
+ self.data = None
+ self.finished = 1
+ if e < 0:
+ # decoding error
+ self.image = None
+ raise _get_oserror(e, encoder=False)
+ else:
+ # end of image
+ return
+ self.data = self.data[n:]
+
+ elif self.image:
+ # if we end up here with no decoder, this file cannot
+ # be incrementally parsed. wait until we've gotten all
+ # available data
+ pass
+
+ else:
+ # attempt to open this file
+ try:
+ with io.BytesIO(self.data) as fp:
+ im = Image.open(fp)
+ except OSError:
+ pass # not enough data
+ else:
+ flag = hasattr(im, "load_seek") or hasattr(im, "load_read")
+ if not flag and len(im.tile) == 1:
+ # initialize decoder
+ im.load_prepare()
+ d, e, o, a = im.tile[0]
+ im.tile = []
+ self.decoder = Image._getdecoder(im.mode, d, a, im.decoderconfig)
+ self.decoder.setimage(im.im, e)
+
+ # calculate decoder offset
+ self.offset = o
+ if self.offset <= len(self.data):
+ self.data = self.data[self.offset :]
+ self.offset = 0
+
+ self.image = im
+
+ def __enter__(self) -> Parser:
+ return self
+
+ def __exit__(self, *args: object) -> None:
+ self.close()
+
+ def close(self) -> Image.Image:
+ """
+ (Consumer) Close the stream.
+
+ :returns: An image object.
+ :exception OSError: If the parser failed to parse the image file either
+ because it cannot be identified or cannot be
+ decoded.
+ """
+ # finish decoding
+ if self.decoder:
+ # get rid of what's left in the buffers
+ self.feed(b"")
+ self.data = self.decoder = None
+ if not self.finished:
+ msg = "image was incomplete"
+ raise OSError(msg)
+ if not self.image:
+ msg = "cannot parse this image"
+ raise OSError(msg)
+ if self.data:
+ # incremental parsing not possible; reopen the file
+ # not that we have all data
+ with io.BytesIO(self.data) as fp:
+ try:
+ self.image = Image.open(fp)
+ finally:
+ self.image.load()
+ return self.image
+
+
+# --------------------------------------------------------------------
+
+
+def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None:
+ """Helper to save image based on tile list
+
+ :param im: Image object.
+ :param fp: File object.
+ :param tile: Tile list.
+ :param bufsize: Optional buffer size
+ """
+
+ im.load()
+ if not hasattr(im, "encoderconfig"):
+ im.encoderconfig = ()
+ tile.sort(key=_tilesort)
+ # FIXME: make MAXBLOCK a configuration parameter
+ # It would be great if we could have the encoder specify what it needs
+ # But, it would need at least the image size in most cases. RawEncode is
+ # a tricky case.
+ bufsize = max(MAXBLOCK, bufsize, im.size[0] * 4) # see RawEncode.c
+ try:
+ fh = fp.fileno()
+ fp.flush()
+ _encode_tile(im, fp, tile, bufsize, fh)
+ except (AttributeError, io.UnsupportedOperation) as exc:
+ _encode_tile(im, fp, tile, bufsize, None, exc)
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+def _encode_tile(
+ im: Image.Image,
+ fp: IO[bytes],
+ tile: list[_Tile],
+ bufsize: int,
+ fh: int | None,
+ exc: BaseException | None = None,
+) -> None:
+ for encoder_name, extents, offset, args in tile:
+ if offset > 0:
+ fp.seek(offset)
+ encoder = Image._getencoder(im.mode, encoder_name, args, im.encoderconfig)
+ try:
+ encoder.setimage(im.im, extents)
+ if encoder.pushes_fd:
+ encoder.setfd(fp)
+ errcode = encoder.encode_to_pyfd()[1]
+ else:
+ if exc:
+ # compress to Python file-compatible object
+ while True:
+ errcode, data = encoder.encode(bufsize)[1:]
+ fp.write(data)
+ if errcode:
+ break
+ else:
+ # slight speedup: compress to real file object
+ assert fh is not None
+ errcode = encoder.encode_to_file(fh, bufsize)
+ if errcode < 0:
+ raise _get_oserror(errcode, encoder=True) from exc
+ finally:
+ encoder.cleanup()
+
+
+def _safe_read(fp: IO[bytes], size: int) -> bytes:
+ """
+ Reads large blocks in a safe way. Unlike fp.read(n), this function
+ doesn't trust the user. If the requested size is larger than
+ SAFEBLOCK, the file is read block by block.
+
+ :param fp: File handle. Must implement a read method.
+ :param size: Number of bytes to read.
+ :returns: A string containing size bytes of data.
+
+ Raises an OSError if the file is truncated and the read cannot be completed
+
+ """
+ if size <= 0:
+ return b""
+ if size <= SAFEBLOCK:
+ data = fp.read(size)
+ if len(data) < size:
+ msg = "Truncated File Read"
+ raise OSError(msg)
+ return data
+ blocks: list[bytes] = []
+ remaining_size = size
+ while remaining_size > 0:
+ block = fp.read(min(remaining_size, SAFEBLOCK))
+ if not block:
+ break
+ blocks.append(block)
+ remaining_size -= len(block)
+ if sum(len(block) for block in blocks) < size:
+ msg = "Truncated File Read"
+ raise OSError(msg)
+ return b"".join(blocks)
+
+
+class PyCodecState:
+ def __init__(self) -> None:
+ self.xsize = 0
+ self.ysize = 0
+ self.xoff = 0
+ self.yoff = 0
+
+ def extents(self) -> tuple[int, int, int, int]:
+ return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
+
+
+class PyCodec:
+ fd: IO[bytes] | None
+
+ def __init__(self, mode: str, *args: Any) -> None:
+ self.im: Image.core.ImagingCore | None = None
+ self.state = PyCodecState()
+ self.fd = None
+ self.mode = mode
+ self.init(args)
+
+ def init(self, args: tuple[Any, ...]) -> None:
+ """
+ Override to perform codec specific initialization
+
+ :param args: Tuple of arg items from the tile entry
+ :returns: None
+ """
+ self.args = args
+
+ def cleanup(self) -> None:
+ """
+ Override to perform codec specific cleanup
+
+ :returns: None
+ """
+ pass
+
+ def setfd(self, fd: IO[bytes]) -> None:
+ """
+ Called from ImageFile to set the Python file-like object
+
+ :param fd: A Python file-like object
+ :returns: None
+ """
+ self.fd = fd
+
+ def setimage(
+ self,
+ im: Image.core.ImagingCore,
+ extents: tuple[int, int, int, int] | None = None,
+ ) -> None:
+ """
+ Called from ImageFile to set the core output image for the codec
+
+ :param im: A core image object
+ :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle
+ for this tile
+ :returns: None
+ """
+
+ # following c code
+ self.im = im
+
+ if extents:
+ x0, y0, x1, y1 = extents
+
+ if x0 < 0 or y0 < 0 or x1 > self.im.size[0] or y1 > self.im.size[1]:
+ msg = "Tile cannot extend outside image"
+ raise ValueError(msg)
+
+ self.state.xoff = x0
+ self.state.yoff = y0
+ self.state.xsize = x1 - x0
+ self.state.ysize = y1 - y0
+ else:
+ self.state.xsize, self.state.ysize = self.im.size
+
+ if self.state.xsize <= 0 or self.state.ysize <= 0:
+ msg = "Size must be positive"
+ raise ValueError(msg)
+
+
+class PyDecoder(PyCodec):
+ """
+ Python implementation of a format decoder. Override this class and
+ add the decoding logic in the :meth:`decode` method.
+
+ See :ref:`Writing Your Own File Codec in Python`
+ """
+
+ _pulls_fd = False
+
+ @property
+ def pulls_fd(self) -> bool:
+ return self._pulls_fd
+
+ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
+ """
+ Override to perform the decoding process.
+
+ :param buffer: A bytes object with the data to be decoded.
+ :returns: A tuple of ``(bytes consumed, errcode)``.
+ If finished with decoding return -1 for the bytes consumed.
+ Err codes are from :data:`.ImageFile.ERRORS`.
+ """
+ msg = "unavailable in base decoder"
+ raise NotImplementedError(msg)
+
+ def set_as_raw(
+ self,
+ data: bytes | bytearray,
+ rawmode: str | None = None,
+ extra: tuple[Any, ...] = (),
+ ) -> None:
+ """
+ Convenience method to set the internal image from a stream of raw data
+
+ :param data: Bytes to be set
+ :param rawmode: The rawmode to be used for the decoder.
+ If not specified, it will default to the mode of the image
+ :param extra: Extra arguments for the decoder.
+ :returns: None
+ """
+
+ if not rawmode:
+ rawmode = self.mode
+ d = Image._getdecoder(self.mode, "raw", rawmode, extra)
+ assert self.im is not None
+ d.setimage(self.im, self.state.extents())
+ s = d.decode(data)
+
+ if s[0] >= 0:
+ msg = "not enough image data"
+ raise ValueError(msg)
+ if s[1] != 0:
+ msg = "cannot decode image data"
+ raise ValueError(msg)
+
+
+class PyEncoder(PyCodec):
+ """
+ Python implementation of a format encoder. Override this class and
+ add the decoding logic in the :meth:`encode` method.
+
+ See :ref:`Writing Your Own File Codec in Python`
+ """
+
+ _pushes_fd = False
+
+ @property
+ def pushes_fd(self) -> bool:
+ return self._pushes_fd
+
+ def encode(self, bufsize: int) -> tuple[int, int, bytes]:
+ """
+ Override to perform the encoding process.
+
+ :param bufsize: Buffer size.
+ :returns: A tuple of ``(bytes encoded, errcode, bytes)``.
+ If finished with encoding return 1 for the error code.
+ Err codes are from :data:`.ImageFile.ERRORS`.
+ """
+ msg = "unavailable in base encoder"
+ raise NotImplementedError(msg)
+
+ def encode_to_pyfd(self) -> tuple[int, int]:
+ """
+ If ``pushes_fd`` is ``True``, then this method will be used,
+ and ``encode()`` will only be called once.
+
+ :returns: A tuple of ``(bytes consumed, errcode)``.
+ Err codes are from :data:`.ImageFile.ERRORS`.
+ """
+ if not self.pushes_fd:
+ return 0, -8 # bad configuration
+ bytes_consumed, errcode, data = self.encode(0)
+ if data:
+ assert self.fd is not None
+ self.fd.write(data)
+ return bytes_consumed, errcode
+
+ def encode_to_file(self, fh: int, bufsize: int) -> int:
+ """
+ :param fh: File handle.
+ :param bufsize: Buffer size.
+
+ :returns: If finished successfully, return 0.
+ Otherwise, return an error code. Err codes are from
+ :data:`.ImageFile.ERRORS`.
+ """
+ errcode = 0
+ while errcode == 0:
+ status, errcode, buf = self.encode(bufsize)
+ if status > 0:
+ os.write(fh, buf[status:])
+ return errcode
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageFilter.py b/venv/lib/python3.11/site-packages/PIL/ImageFilter.py
new file mode 100644
index 000000000..823365af0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageFilter.py
@@ -0,0 +1,617 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# standard filters
+#
+# History:
+# 1995-11-27 fl Created
+# 2002-06-08 fl Added rank and mode filters
+# 2003-09-15 fl Fixed rank calculation in rank filter; added expand call
+#
+# Copyright (c) 1997-2003 by Secret Labs AB.
+# Copyright (c) 1995-2002 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import abc
+import functools
+from collections.abc import Sequence
+from typing import cast
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from types import ModuleType
+ from typing import Any
+
+ from . import _imaging
+ from ._typing import NumpyArray
+
+
+class Filter(abc.ABC):
+ @abc.abstractmethod
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ pass
+
+
+class MultibandFilter(Filter):
+ pass
+
+
+class BuiltinFilter(MultibandFilter):
+ name: str
+ filterargs: tuple[Any, ...]
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ if image.mode == "P":
+ msg = "cannot filter palette images"
+ raise ValueError(msg)
+ return image.filter(*self.filterargs)
+
+
+class Kernel(BuiltinFilter):
+ """
+ Create a convolution kernel. This only supports 3x3 and 5x5 integer and floating
+ point kernels.
+
+ Kernels can only be applied to "L" and "RGB" images.
+
+ :param size: Kernel size, given as (width, height). This must be (3,3) or (5,5).
+ :param kernel: A sequence containing kernel weights. The kernel will be flipped
+ vertically before being applied to the image.
+ :param scale: Scale factor. If given, the result for each pixel is divided by this
+ value. The default is the sum of the kernel weights.
+ :param offset: Offset. If given, this value is added to the result, after it has
+ been divided by the scale factor.
+ """
+
+ name = "Kernel"
+
+ def __init__(
+ self,
+ size: tuple[int, int],
+ kernel: Sequence[float],
+ scale: float | None = None,
+ offset: float = 0,
+ ) -> None:
+ if scale is None:
+ # default scale is sum of kernel
+ scale = functools.reduce(lambda a, b: a + b, kernel)
+ if size[0] * size[1] != len(kernel):
+ msg = "not enough coefficients in kernel"
+ raise ValueError(msg)
+ self.filterargs = size, scale, offset, kernel
+
+
+class RankFilter(Filter):
+ """
+ Create a rank filter. The rank filter sorts all pixels in
+ a window of the given size, and returns the ``rank``'th value.
+
+ :param size: The kernel size, in pixels.
+ :param rank: What pixel value to pick. Use 0 for a min filter,
+ ``size * size / 2`` for a median filter, ``size * size - 1``
+ for a max filter, etc.
+ """
+
+ name = "Rank"
+
+ def __init__(self, size: int, rank: int) -> None:
+ if size % 2 == 0:
+ msg = "bad filter size"
+ raise ValueError(msg)
+ if size * size * 4 > (2**31 - 1):
+ msg = "filter size too large"
+ raise ValueError(msg)
+ if rank < 0 or rank >= size * size:
+ msg = "bad rank value"
+ raise ValueError(msg)
+ self.size = size
+ self.rank = rank
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ if image.mode == "P":
+ msg = "cannot filter palette images"
+ raise ValueError(msg)
+ image = image.expand(self.size // 2)
+ return image.rankfilter(self.size, self.rank)
+
+
+class MedianFilter(RankFilter):
+ """
+ Create a median filter. Picks the median pixel value in a window with the
+ given size.
+
+ :param size: The kernel size, in pixels.
+ """
+
+ name = "Median"
+
+ def __init__(self, size: int = 3) -> None:
+ rank = size * size // 2
+ super().__init__(size, rank)
+
+
+class MinFilter(RankFilter):
+ """
+ Create a min filter. Picks the lowest pixel value in a window with the
+ given size.
+
+ :param size: The kernel size, in pixels.
+ """
+
+ name = "Min"
+
+ def __init__(self, size: int = 3) -> None:
+ rank = 0
+ super().__init__(size, rank)
+
+
+class MaxFilter(RankFilter):
+ """
+ Create a max filter. Picks the largest pixel value in a window with the
+ given size.
+
+ :param size: The kernel size, in pixels.
+ """
+
+ name = "Max"
+
+ def __init__(self, size: int = 3) -> None:
+ rank = size * size - 1
+ super().__init__(size, rank)
+
+
+class ModeFilter(Filter):
+ """
+ Create a mode filter. Picks the most frequent pixel value in a box with the
+ given size. Pixel values that occur only once or twice are ignored; if no
+ pixel value occurs more than twice, the original pixel value is preserved.
+
+ :param size: The kernel size, in pixels.
+ """
+
+ name = "Mode"
+
+ def __init__(self, size: int = 3) -> None:
+ self.size = size
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ return image.modefilter(self.size)
+
+
+class GaussianBlur(MultibandFilter):
+ """Blurs the image with a sequence of extended box filters, which
+ approximates a Gaussian kernel. For details on accuracy see
+
+
+ :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two
+ numbers for x and y, or a single number for both.
+ """
+
+ name = "GaussianBlur"
+
+ def __init__(self, radius: float | Sequence[float] = 2) -> None:
+ self.radius = radius
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ xy = self.radius
+ if xy == (0, 0) or xy == 0:
+ return image.copy()
+ if isinstance(xy, (int, float)):
+ xy = (xy, xy)
+ return image.gaussian_blur(xy)
+
+
+class BoxBlur(MultibandFilter):
+ """Blurs the image by setting each pixel to the average value of the pixels
+ in a square box extending radius pixels in each direction.
+ Supports float radius of arbitrary size. Uses an optimized implementation
+ which runs in linear time relative to the size of the image
+ for any radius value.
+
+ :param radius: Size of the box in a direction. Either a sequence of two numbers for
+ x and y, or a single number for both.
+
+ Radius 0 does not blur, returns an identical image.
+ Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total.
+ """
+
+ name = "BoxBlur"
+
+ def __init__(self, radius: float | Sequence[float]) -> None:
+ xy = radius if isinstance(radius, (tuple, list)) else (radius, radius)
+ if xy[0] < 0 or xy[1] < 0:
+ msg = "radius must be >= 0"
+ raise ValueError(msg)
+ self.radius = radius
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ xy = self.radius
+ if xy == (0, 0) or xy == 0:
+ return image.copy()
+ if isinstance(xy, (int, float)):
+ xy = (xy, xy)
+ return image.box_blur(xy)
+
+
+class UnsharpMask(MultibandFilter):
+ """Unsharp mask filter.
+
+ See Wikipedia's entry on `digital unsharp masking`_ for an explanation of
+ the parameters.
+
+ :param radius: Blur Radius
+ :param percent: Unsharp strength, in percent
+ :param threshold: Threshold controls the minimum brightness change that
+ will be sharpened
+
+ .. _digital unsharp masking: https://en.wikipedia.org/wiki/Unsharp_masking#Digital_unsharp_masking
+
+ """
+
+ name = "UnsharpMask"
+
+ def __init__(
+ self, radius: float = 2, percent: int = 150, threshold: int = 3
+ ) -> None:
+ self.radius = radius
+ self.percent = percent
+ self.threshold = threshold
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ return image.unsharp_mask(self.radius, self.percent, self.threshold)
+
+
+class BLUR(BuiltinFilter):
+ name = "Blur"
+ # fmt: off
+ filterargs = (5, 5), 16, 0, (
+ 1, 1, 1, 1, 1,
+ 1, 0, 0, 0, 1,
+ 1, 0, 0, 0, 1,
+ 1, 0, 0, 0, 1,
+ 1, 1, 1, 1, 1,
+ )
+ # fmt: on
+
+
+class CONTOUR(BuiltinFilter):
+ name = "Contour"
+ # fmt: off
+ filterargs = (3, 3), 1, 255, (
+ -1, -1, -1,
+ -1, 8, -1,
+ -1, -1, -1,
+ )
+ # fmt: on
+
+
+class DETAIL(BuiltinFilter):
+ name = "Detail"
+ # fmt: off
+ filterargs = (3, 3), 6, 0, (
+ 0, -1, 0,
+ -1, 10, -1,
+ 0, -1, 0,
+ )
+ # fmt: on
+
+
+class EDGE_ENHANCE(BuiltinFilter):
+ name = "Edge-enhance"
+ # fmt: off
+ filterargs = (3, 3), 2, 0, (
+ -1, -1, -1,
+ -1, 10, -1,
+ -1, -1, -1,
+ )
+ # fmt: on
+
+
+class EDGE_ENHANCE_MORE(BuiltinFilter):
+ name = "Edge-enhance More"
+ # fmt: off
+ filterargs = (3, 3), 1, 0, (
+ -1, -1, -1,
+ -1, 9, -1,
+ -1, -1, -1,
+ )
+ # fmt: on
+
+
+class EMBOSS(BuiltinFilter):
+ name = "Emboss"
+ # fmt: off
+ filterargs = (3, 3), 1, 128, (
+ -1, 0, 0,
+ 0, 1, 0,
+ 0, 0, 0,
+ )
+ # fmt: on
+
+
+class FIND_EDGES(BuiltinFilter):
+ name = "Find Edges"
+ # fmt: off
+ filterargs = (3, 3), 1, 0, (
+ -1, -1, -1,
+ -1, 8, -1,
+ -1, -1, -1,
+ )
+ # fmt: on
+
+
+class SHARPEN(BuiltinFilter):
+ name = "Sharpen"
+ # fmt: off
+ filterargs = (3, 3), 16, 0, (
+ -2, -2, -2,
+ -2, 32, -2,
+ -2, -2, -2,
+ )
+ # fmt: on
+
+
+class SMOOTH(BuiltinFilter):
+ name = "Smooth"
+ # fmt: off
+ filterargs = (3, 3), 13, 0, (
+ 1, 1, 1,
+ 1, 5, 1,
+ 1, 1, 1,
+ )
+ # fmt: on
+
+
+class SMOOTH_MORE(BuiltinFilter):
+ name = "Smooth More"
+ # fmt: off
+ filterargs = (5, 5), 100, 0, (
+ 1, 1, 1, 1, 1,
+ 1, 5, 5, 5, 1,
+ 1, 5, 44, 5, 1,
+ 1, 5, 5, 5, 1,
+ 1, 1, 1, 1, 1,
+ )
+ # fmt: on
+
+
+class Color3DLUT(MultibandFilter):
+ """Three-dimensional color lookup table.
+
+ Transforms 3-channel pixels using the values of the channels as coordinates
+ in the 3D lookup table and interpolating the nearest elements.
+
+ This method allows you to apply almost any color transformation
+ in constant time by using pre-calculated decimated tables.
+
+ .. versionadded:: 5.2.0
+
+ :param size: Size of the table. One int or tuple of (int, int, int).
+ Minimal size in any dimension is 2, maximum is 65.
+ :param table: Flat lookup table. A list of ``channels * size**3``
+ float elements or a list of ``size**3`` channels-sized
+ tuples with floats. Channels are changed first,
+ then first dimension, then second, then third.
+ Value 0.0 corresponds lowest value of output, 1.0 highest.
+ :param channels: Number of channels in the table. Could be 3 or 4.
+ Default is 3.
+ :param target_mode: A mode for the result image. Should have not less
+ than ``channels`` channels. Default is ``None``,
+ which means that mode wouldn't be changed.
+ """
+
+ name = "Color 3D LUT"
+
+ def __init__(
+ self,
+ size: int | tuple[int, int, int],
+ table: Sequence[float] | Sequence[Sequence[int]] | NumpyArray,
+ channels: int = 3,
+ target_mode: str | None = None,
+ **kwargs: bool,
+ ) -> None:
+ if channels not in (3, 4):
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
+ self.size = size = self._check_size(size)
+ self.channels = channels
+ self.mode = target_mode
+
+ # Hidden flag `_copy_table=False` could be used to avoid extra copying
+ # of the table if the table is specially made for the constructor.
+ copy_table = kwargs.get("_copy_table", True)
+ items = size[0] * size[1] * size[2]
+ wrong_size = False
+
+ numpy: ModuleType | None = None
+ if hasattr(table, "shape"):
+ try:
+ import numpy
+ except ImportError:
+ pass
+
+ if numpy and isinstance(table, numpy.ndarray):
+ numpy_table: NumpyArray = table
+ if copy_table:
+ numpy_table = numpy_table.copy()
+
+ if numpy_table.shape in [
+ (items * channels,),
+ (items, channels),
+ (size[2], size[1], size[0], channels),
+ ]:
+ table = numpy_table.reshape(items * channels)
+ else:
+ wrong_size = True
+
+ else:
+ if copy_table:
+ table = list(table)
+
+ # Convert to a flat list
+ if table and isinstance(table[0], (list, tuple)):
+ raw_table = cast(Sequence[Sequence[int]], table)
+ flat_table: list[int] = []
+ for pixel in raw_table:
+ if len(pixel) != channels:
+ msg = (
+ "The elements of the table should "
+ f"have a length of {channels}."
+ )
+ raise ValueError(msg)
+ flat_table.extend(pixel)
+ table = flat_table
+
+ if wrong_size or len(table) != items * channels:
+ msg = (
+ "The table should have either channels * size**3 float items "
+ "or size**3 items of channels-sized tuples with floats. "
+ f"Table should be: {channels}x{size[0]}x{size[1]}x{size[2]}. "
+ f"Actual length: {len(table)}"
+ )
+ raise ValueError(msg)
+ self.table = table
+
+ @staticmethod
+ def _check_size(size: Any) -> tuple[int, int, int]:
+ try:
+ _, _, _ = size
+ except ValueError as e:
+ msg = "Size should be either an integer or a tuple of three integers."
+ raise ValueError(msg) from e
+ except TypeError:
+ size = (size, size, size)
+ size = tuple(int(x) for x in size)
+ for size_1d in size:
+ if not 2 <= size_1d <= 65:
+ msg = "Size should be in [2, 65] range."
+ raise ValueError(msg)
+ return size
+
+ @classmethod
+ def generate(
+ cls,
+ size: int | tuple[int, int, int],
+ callback: Callable[[float, float, float], tuple[float, ...]],
+ channels: int = 3,
+ target_mode: str | None = None,
+ ) -> Color3DLUT:
+ """Generates new LUT using provided callback.
+
+ :param size: Size of the table. Passed to the constructor.
+ :param callback: Function with three parameters which correspond
+ three color channels. Will be called ``size**3``
+ times with values from 0.0 to 1.0 and should return
+ a tuple with ``channels`` elements.
+ :param channels: The number of channels which should return callback.
+ :param target_mode: Passed to the constructor of the resulting
+ lookup table.
+ """
+ size_1d, size_2d, size_3d = cls._check_size(size)
+ if channels not in (3, 4):
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
+
+ table: list[float] = [0] * (size_1d * size_2d * size_3d * channels)
+ idx_out = 0
+ for b in range(size_3d):
+ for g in range(size_2d):
+ for r in range(size_1d):
+ table[idx_out : idx_out + channels] = callback(
+ r / (size_1d - 1), g / (size_2d - 1), b / (size_3d - 1)
+ )
+ idx_out += channels
+
+ return cls(
+ (size_1d, size_2d, size_3d),
+ table,
+ channels=channels,
+ target_mode=target_mode,
+ _copy_table=False,
+ )
+
+ def transform(
+ self,
+ callback: Callable[..., tuple[float, ...]],
+ with_normals: bool = False,
+ channels: int | None = None,
+ target_mode: str | None = None,
+ ) -> Color3DLUT:
+ """Transforms the table values using provided callback and returns
+ a new LUT with altered values.
+
+ :param callback: A function which takes old lookup table values
+ and returns a new set of values. The number
+ of arguments which function should take is
+ ``self.channels`` or ``3 + self.channels``
+ if ``with_normals`` flag is set.
+ Should return a tuple of ``self.channels`` or
+ ``channels`` elements if it is set.
+ :param with_normals: If true, ``callback`` will be called with
+ coordinates in the color cube as the first
+ three arguments. Otherwise, ``callback``
+ will be called only with actual color values.
+ :param channels: The number of channels in the resulting lookup table.
+ :param target_mode: Passed to the constructor of the resulting
+ lookup table.
+ """
+ if channels not in (None, 3, 4):
+ msg = "Only 3 or 4 output channels are supported"
+ raise ValueError(msg)
+ ch_in = self.channels
+ ch_out = channels or ch_in
+ size_1d, size_2d, size_3d = self.size
+
+ table: list[float] = [0] * (size_1d * size_2d * size_3d * ch_out)
+ idx_in = 0
+ idx_out = 0
+ for b in range(size_3d):
+ for g in range(size_2d):
+ for r in range(size_1d):
+ values = self.table[idx_in : idx_in + ch_in]
+ if with_normals:
+ values = callback(
+ r / (size_1d - 1),
+ g / (size_2d - 1),
+ b / (size_3d - 1),
+ *values,
+ )
+ else:
+ values = callback(*values)
+ table[idx_out : idx_out + ch_out] = values
+ idx_in += ch_in
+ idx_out += ch_out
+
+ return type(self)(
+ self.size,
+ table,
+ channels=ch_out,
+ target_mode=target_mode or self.mode,
+ _copy_table=False,
+ )
+
+ def __repr__(self) -> str:
+ r = [
+ f"{self.__class__.__name__} from {self.table.__class__.__name__}",
+ "size={:d}x{:d}x{:d}".format(*self.size),
+ f"channels={self.channels:d}",
+ ]
+ if self.mode:
+ r.append(f"target_mode={self.mode}")
+ return "<{}>".format(" ".join(r))
+
+ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
+ from . import Image
+
+ return image.color_lut_3d(
+ self.mode or image.mode,
+ Image.Resampling.BILINEAR,
+ self.channels,
+ self.size,
+ self.table,
+ )
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageFont.py b/venv/lib/python3.11/site-packages/PIL/ImageFont.py
new file mode 100644
index 000000000..65edf5659
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageFont.py
@@ -0,0 +1,1330 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# PIL raster font management
+#
+# History:
+# 1996-08-07 fl created (experimental)
+# 1997-08-25 fl minor adjustments to handle fonts from pilfont 0.3
+# 1999-02-06 fl rewrote most font management stuff in C
+# 1999-03-17 fl take pth files into account in load_path (from Richard Jones)
+# 2001-02-17 fl added freetype support
+# 2001-05-09 fl added TransposedFont wrapper class
+# 2002-03-04 fl make sure we have a "L" or "1" font
+# 2002-12-04 fl skip non-directory entries in the system path
+# 2003-04-29 fl add embedded default font
+# 2003-09-27 fl added support for truetype charmap encodings
+#
+# Todo:
+# Adapt to PILFONT2 format (16-bit fonts, compressed, single file)
+#
+# Copyright (c) 1997-2003 by Secret Labs AB
+# Copyright (c) 1996-2003 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+
+from __future__ import annotations
+
+import abc
+import base64
+import os
+import sys
+import warnings
+from enum import IntEnum
+from io import BytesIO
+from types import ModuleType
+from typing import IO, Any, BinaryIO, TypedDict, cast
+
+from . import Image
+from ._typing import StrOrBytesPath
+from ._util import DeferredError, is_path
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from . import ImageFile
+ from ._imaging import ImagingFont
+ from ._imagingft import Font
+
+
+class Axis(TypedDict):
+ minimum: int | None
+ default: int | None
+ maximum: int | None
+ name: bytes | None
+
+
+class Layout(IntEnum):
+ BASIC = 0
+ RAQM = 1
+
+
+MAX_STRING_LENGTH = 1_000_000
+
+
+core: ModuleType | DeferredError
+try:
+ from . import _imagingft as core
+except ImportError as ex:
+ core = DeferredError.new(ex)
+
+
+def _string_length_check(text: str | bytes | bytearray) -> None:
+ if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH:
+ msg = "too many characters in string"
+ raise ValueError(msg)
+
+
+# FIXME: add support for pilfont2 format (see FontFile.py)
+
+# --------------------------------------------------------------------
+# Font metrics format:
+# "PILfont" LF
+# fontdescriptor LF
+# (optional) key=value... LF
+# "DATA" LF
+# binary data: 256*10*2 bytes (dx, dy, dstbox, srcbox)
+#
+# To place a character, cut out srcbox and paste at dstbox,
+# relative to the character position. Then move the character
+# position according to dx, dy.
+# --------------------------------------------------------------------
+
+
+class BaseImageFont(abc.ABC):
+ """Used by ImageDraw and ImageText"""
+
+ @abc.abstractmethod
+ def getbbox(
+ self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
+ ) -> tuple[float, float, float, float]:
+ pass
+
+ @abc.abstractmethod
+ def getmask(
+ self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
+ ) -> Image.core.ImagingCore:
+ pass
+
+ @abc.abstractmethod
+ def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float:
+ pass
+
+
+class ImageFont(BaseImageFont):
+ """PIL font wrapper"""
+
+ font: ImagingFont
+
+ def _load_pilfont(self, filename: str) -> None:
+ with open(filename, "rb") as fp:
+ image: ImageFile.ImageFile | None = None
+ root = os.path.splitext(filename)[0]
+
+ for ext in (".png", ".gif", ".pbm"):
+ if image:
+ image.close()
+ try:
+ fullname = root + ext
+ image = Image.open(fullname)
+ except Exception:
+ pass
+ else:
+ if image.mode in ("1", "L"):
+ break
+ else:
+ if image:
+ image.close()
+
+ msg = f"cannot find glyph data file {root}.{{gif|pbm|png}}"
+ raise OSError(msg)
+
+ self.file = fullname
+
+ self._load_pilfont_data(fp, image)
+ image.close()
+
+ def _load_pilfont_data(self, file: IO[bytes], image: Image.Image) -> None:
+ # check image
+ if image.mode not in ("1", "L"):
+ image.close()
+
+ msg = "invalid font image mode"
+ raise TypeError(msg)
+
+ # read PILfont header
+ if file.read(8) != b"PILfont\n":
+ image.close()
+
+ msg = "Not a PILfont file"
+ raise SyntaxError(msg)
+ file.readline()
+ self.info = [] # FIXME: should be a dictionary
+ while True:
+ s = file.readline()
+ if not s or s == b"DATA\n":
+ break
+ self.info.append(s)
+
+ # read PILfont metrics
+ data = file.read(256 * 20)
+
+ self._load(image, data)
+
+ def _load(self, image: Image.Image, data: bytes) -> None:
+ image.load()
+
+ self.font = Image.core.font(image.im, data)
+
+ def getmask(
+ self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
+ ) -> Image.core.ImagingCore:
+ """
+ Create a bitmap for the text.
+
+ If the font uses antialiasing, the bitmap should have mode ``L`` and use a
+ maximum value of 255. Otherwise, it should have mode ``1``.
+
+ :param text: Text to render.
+ :param mode: Used by some graphics drivers to indicate what mode the
+ driver prefers; if empty, the renderer may return either
+ mode. Note that the mode is always a string, to simplify
+ C-level implementations.
+
+ .. versionadded:: 1.1.5
+
+ :return: An internal PIL storage memory instance as defined by the
+ :py:mod:`PIL.Image.core` interface module.
+ """
+ _string_length_check(text)
+ Image._decompression_bomb_check(self.font.getsize(text))
+ return self.font.getmask(text, mode)
+
+ def getbbox(
+ self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
+ ) -> tuple[int, int, int, int]:
+ """
+ Returns bounding box (in pixels) of given text.
+
+ .. versionadded:: 9.2.0
+
+ :param text: Text to render.
+
+ :return: ``(left, top, right, bottom)`` bounding box
+ """
+ _string_length_check(text)
+ width, height = self.font.getsize(text)
+ return 0, 0, width, height
+
+ def getlength(
+ self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
+ ) -> int:
+ """
+ Returns length (in pixels) of given text.
+ This is the amount by which following text should be offset.
+
+ .. versionadded:: 9.2.0
+ """
+ _string_length_check(text)
+ width, height = self.font.getsize(text)
+ return width
+
+
+##
+# Wrapper for FreeType fonts. Application code should use the
+# truetype factory function to create font objects.
+
+
+class FreeTypeFont(BaseImageFont):
+ """FreeType font wrapper (requires _imagingft service)"""
+
+ font: Font
+ font_bytes: bytes
+
+ def __init__(
+ self,
+ font: StrOrBytesPath | BinaryIO,
+ size: float = 10,
+ index: int = 0,
+ encoding: str = "",
+ layout_engine: Layout | None = None,
+ ) -> None:
+ # FIXME: use service provider instead
+
+ if isinstance(core, DeferredError):
+ raise core.ex
+
+ if size <= 0:
+ msg = f"font size must be greater than 0, not {size}"
+ raise ValueError(msg)
+
+ self.path = font
+ self.size = size
+ self.index = index
+ self.encoding = encoding
+
+ if layout_engine not in (Layout.BASIC, Layout.RAQM):
+ layout_engine = Layout.BASIC
+ if core.HAVE_RAQM:
+ layout_engine = Layout.RAQM
+ elif layout_engine == Layout.RAQM and not core.HAVE_RAQM:
+ warnings.warn(
+ "Raqm layout was requested, but Raqm is not available. "
+ "Falling back to basic layout."
+ )
+ layout_engine = Layout.BASIC
+
+ self.layout_engine = layout_engine
+
+ def load_from_bytes(f: IO[bytes]) -> None:
+ self.font_bytes = f.read()
+ self.font = core.getfont(
+ "", size, index, encoding, self.font_bytes, layout_engine
+ )
+
+ if is_path(font):
+ font = os.fspath(font)
+ if sys.platform == "win32":
+ font_bytes_path = font if isinstance(font, bytes) else font.encode()
+ try:
+ font_bytes_path.decode("ascii")
+ except UnicodeDecodeError:
+ # FreeType cannot load fonts with non-ASCII characters on Windows
+ # So load it into memory first
+ with open(font, "rb") as f:
+ load_from_bytes(f)
+ return
+ self.font = core.getfont(
+ font, size, index, encoding, layout_engine=layout_engine
+ )
+ else:
+ load_from_bytes(cast(IO[bytes], font))
+
+ def __getstate__(self) -> list[Any]:
+ return [self.path, self.size, self.index, self.encoding, self.layout_engine]
+
+ def __setstate__(self, state: list[Any]) -> None:
+ path, size, index, encoding, layout_engine = state
+ FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine)
+
+ def getname(self) -> tuple[str | None, str | None]:
+ """
+ :return: A tuple of the font family (e.g. Helvetica) and the font style
+ (e.g. Bold)
+ """
+ return self.font.family, self.font.style
+
+ def getmetrics(self) -> tuple[int, int]:
+ """
+ :return: A tuple of the font ascent (the distance from the baseline to
+ the highest outline point) and descent (the distance from the
+ baseline to the lowest outline point, a negative value)
+ """
+ return self.font.ascent, self.font.descent
+
+ def getlength(
+ self,
+ text: str | bytes,
+ mode: str = "",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ ) -> float:
+ """
+ Returns length (in pixels with 1/64 precision) of given text when rendered
+ in font with provided direction, features, and language.
+
+ This is the amount by which following text should be offset.
+ Text bounding box may extend past the length in some fonts,
+ e.g. when using italics or accents.
+
+ The result is returned as a float; it is a whole number if using basic layout.
+
+ Note that the sum of two lengths may not equal the length of a concatenated
+ string due to kerning. If you need to adjust for kerning, include the following
+ character and subtract its length.
+
+ For example, instead of ::
+
+ hello = font.getlength("Hello")
+ world = font.getlength("World")
+ hello_world = hello + world # not adjusted for kerning
+ assert hello_world == font.getlength("HelloWorld") # may fail
+
+ use ::
+
+ hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning
+ world = font.getlength("World")
+ hello_world = hello + world # adjusted for kerning
+ assert hello_world == font.getlength("HelloWorld") # True
+
+ or disable kerning with (requires libraqm) ::
+
+ hello = draw.textlength("Hello", font, features=["-kern"])
+ world = draw.textlength("World", font, features=["-kern"])
+ hello_world = hello + world # kerning is disabled, no need to adjust
+ assert hello_world == draw.textlength("HelloWorld", font, features=["-kern"])
+
+ .. versionadded:: 8.0.0
+
+ :param text: Text to measure.
+ :param mode: Used by some graphics drivers to indicate what mode the
+ driver prefers; if empty, the renderer may return either
+ mode. Note that the mode is always a string, to simplify
+ C-level implementations.
+
+ :param direction: Direction of the text. It can be 'rtl' (right to
+ left), 'ltr' (left to right) or 'ttb' (top to bottom).
+ Requires libraqm.
+
+ :param features: A list of OpenType font features to be used during text
+ layout. This is usually used to turn on optional
+ font features that are not enabled by default,
+ for example 'dlig' or 'ss01', but can be also
+ used to turn off default font features for
+ example '-liga' to disable ligatures or '-kern'
+ to disable kerning. To get all supported
+ features, see
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
+ Requires libraqm.
+
+ :param language: Language of the text. Different languages may use
+ different glyph shapes or ligatures. This parameter tells
+ the font which language the text is in, and to apply the
+ correct substitutions as appropriate, if available.
+ It should be a `BCP 47 language code
+ `_
+ Requires libraqm.
+
+ :return: Either width for horizontal text, or height for vertical text.
+ """
+ _string_length_check(text)
+ return self.font.getlength(text, mode, direction, features, language) / 64
+
+ def getbbox(
+ self,
+ text: str | bytes | bytearray,
+ mode: str = "",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ anchor: str | None = None,
+ ) -> tuple[float, float, float, float]:
+ """
+ Returns bounding box (in pixels) of given text relative to given anchor
+ when rendered in font with provided direction, features, and language.
+
+ Use :py:meth:`getlength()` to get the offset of following text with
+ 1/64 pixel precision. The bounding box includes extra margins for
+ some fonts, e.g. italics or accents.
+
+ .. versionadded:: 8.0.0
+
+ :param text: Text to render.
+ :param mode: Used by some graphics drivers to indicate what mode the
+ driver prefers; if empty, the renderer may return either
+ mode. Note that the mode is always a string, to simplify
+ C-level implementations.
+
+ :param direction: Direction of the text. It can be 'rtl' (right to
+ left), 'ltr' (left to right) or 'ttb' (top to bottom).
+ Requires libraqm.
+
+ :param features: A list of OpenType font features to be used during text
+ layout. This is usually used to turn on optional
+ font features that are not enabled by default,
+ for example 'dlig' or 'ss01', but can be also
+ used to turn off default font features for
+ example '-liga' to disable ligatures or '-kern'
+ to disable kerning. To get all supported
+ features, see
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
+ Requires libraqm.
+
+ :param language: Language of the text. Different languages may use
+ different glyph shapes or ligatures. This parameter tells
+ the font which language the text is in, and to apply the
+ correct substitutions as appropriate, if available.
+ It should be a `BCP 47 language code
+ `_
+ Requires libraqm.
+
+ :param stroke_width: The width of the text stroke.
+
+ :param anchor: The text anchor alignment. Determines the relative location of
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+
+ :return: ``(left, top, right, bottom)`` bounding box
+ """
+ _string_length_check(text)
+ size, offset = self.font.getsize(
+ text, mode, direction, features, language, anchor
+ )
+ left, top = offset[0] - stroke_width, offset[1] - stroke_width
+ width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width
+ return left, top, left + width, top + height
+
+ def getmask(
+ self,
+ text: str | bytes,
+ mode: str = "",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ anchor: str | None = None,
+ ink: int = 0,
+ start: tuple[float, float] | None = None,
+ ) -> Image.core.ImagingCore:
+ """
+ Create a bitmap for the text.
+
+ If the font uses antialiasing, the bitmap should have mode ``L`` and use a
+ maximum value of 255. If the font has embedded color data, the bitmap
+ should have mode ``RGBA``. Otherwise, it should have mode ``1``.
+
+ :param text: Text to render.
+ :param mode: Used by some graphics drivers to indicate what mode the
+ driver prefers; if empty, the renderer may return either
+ mode. Note that the mode is always a string, to simplify
+ C-level implementations.
+
+ .. versionadded:: 1.1.5
+
+ :param direction: Direction of the text. It can be 'rtl' (right to
+ left), 'ltr' (left to right) or 'ttb' (top to bottom).
+ Requires libraqm.
+
+ .. versionadded:: 4.2.0
+
+ :param features: A list of OpenType font features to be used during text
+ layout. This is usually used to turn on optional
+ font features that are not enabled by default,
+ for example 'dlig' or 'ss01', but can be also
+ used to turn off default font features for
+ example '-liga' to disable ligatures or '-kern'
+ to disable kerning. To get all supported
+ features, see
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
+ Requires libraqm.
+
+ .. versionadded:: 4.2.0
+
+ :param language: Language of the text. Different languages may use
+ different glyph shapes or ligatures. This parameter tells
+ the font which language the text is in, and to apply the
+ correct substitutions as appropriate, if available.
+ It should be a `BCP 47 language code
+ `_
+ Requires libraqm.
+
+ .. versionadded:: 6.0.0
+
+ :param stroke_width: The width of the text stroke.
+
+ .. versionadded:: 6.2.0
+
+ :param anchor: The text anchor alignment. Determines the relative location of
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+
+ .. versionadded:: 8.0.0
+
+ :param ink: Foreground ink for rendering in RGBA mode.
+
+ .. versionadded:: 8.0.0
+
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
+ :return: An internal PIL storage memory instance as defined by the
+ :py:mod:`PIL.Image.core` interface module.
+ """
+ return self.getmask2(
+ text,
+ mode,
+ direction=direction,
+ features=features,
+ language=language,
+ stroke_width=stroke_width,
+ anchor=anchor,
+ ink=ink,
+ start=start,
+ )[0]
+
+ def getmask2(
+ self,
+ text: str | bytes,
+ mode: str = "",
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ stroke_width: float = 0,
+ anchor: str | None = None,
+ ink: int = 0,
+ start: tuple[float, float] | None = None,
+ *args: Any,
+ **kwargs: Any,
+ ) -> tuple[Image.core.ImagingCore, tuple[int, int]]:
+ """
+ Create a bitmap for the text.
+
+ If the font uses antialiasing, the bitmap should have mode ``L`` and use a
+ maximum value of 255. If the font has embedded color data, the bitmap
+ should have mode ``RGBA``. Otherwise, it should have mode ``1``.
+
+ :param text: Text to render.
+ :param mode: Used by some graphics drivers to indicate what mode the
+ driver prefers; if empty, the renderer may return either
+ mode. Note that the mode is always a string, to simplify
+ C-level implementations.
+
+ .. versionadded:: 1.1.5
+
+ :param direction: Direction of the text. It can be 'rtl' (right to
+ left), 'ltr' (left to right) or 'ttb' (top to bottom).
+ Requires libraqm.
+
+ .. versionadded:: 4.2.0
+
+ :param features: A list of OpenType font features to be used during text
+ layout. This is usually used to turn on optional
+ font features that are not enabled by default,
+ for example 'dlig' or 'ss01', but can be also
+ used to turn off default font features for
+ example '-liga' to disable ligatures or '-kern'
+ to disable kerning. To get all supported
+ features, see
+ https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
+ Requires libraqm.
+
+ .. versionadded:: 4.2.0
+
+ :param language: Language of the text. Different languages may use
+ different glyph shapes or ligatures. This parameter tells
+ the font which language the text is in, and to apply the
+ correct substitutions as appropriate, if available.
+ It should be a `BCP 47 language code
+ `_
+ Requires libraqm.
+
+ .. versionadded:: 6.0.0
+
+ :param stroke_width: The width of the text stroke.
+
+ .. versionadded:: 6.2.0
+
+ :param anchor: The text anchor alignment. Determines the relative location of
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+
+ .. versionadded:: 8.0.0
+
+ :param ink: Foreground ink for rendering in RGBA mode.
+
+ .. versionadded:: 8.0.0
+
+ :param start: Tuple of horizontal and vertical offset, as text may render
+ differently when starting at fractional coordinates.
+
+ .. versionadded:: 9.4.0
+
+ :return: A tuple of an internal PIL storage memory instance as defined by the
+ :py:mod:`PIL.Image.core` interface module, and the text offset, the
+ gap between the starting coordinate and the first marking
+ """
+ _string_length_check(text)
+ if start is None:
+ start = (0, 0)
+
+ def fill(width: int, height: int) -> Image.core.ImagingCore:
+ size = (width, height)
+ Image._decompression_bomb_check(size)
+ return Image.core.fill("RGBA" if mode == "RGBA" else "L", size)
+
+ return self.font.render(
+ text,
+ fill,
+ mode,
+ direction,
+ features,
+ language,
+ stroke_width,
+ kwargs.get("stroke_filled", False),
+ anchor,
+ ink,
+ start,
+ )
+
+ def font_variant(
+ self,
+ font: StrOrBytesPath | BinaryIO | None = None,
+ size: float | None = None,
+ index: int | None = None,
+ encoding: str | None = None,
+ layout_engine: Layout | None = None,
+ ) -> FreeTypeFont:
+ """
+ Create a copy of this FreeTypeFont object,
+ using any specified arguments to override the settings.
+
+ Parameters are identical to the parameters used to initialize this
+ object.
+
+ :return: A FreeTypeFont object.
+ """
+ if font is None:
+ try:
+ font = BytesIO(self.font_bytes)
+ except AttributeError:
+ font = self.path
+ return FreeTypeFont(
+ font=font,
+ size=self.size if size is None else size,
+ index=self.index if index is None else index,
+ encoding=self.encoding if encoding is None else encoding,
+ layout_engine=layout_engine or self.layout_engine,
+ )
+
+ def get_variation_names(self) -> list[bytes]:
+ """
+ :returns: A list of the named styles in a variation font.
+ :exception OSError: If the font is not a variation font.
+ """
+ names = []
+ for name in self.font.getvarnames():
+ name = name.replace(b"\x00", b"")
+ if name not in names:
+ names.append(name)
+ return names
+
+ def set_variation_by_name(self, name: str | bytes) -> None:
+ """
+ :param name: The name of the style.
+ :exception OSError: If the font is not a variation font.
+ """
+ names = self.get_variation_names()
+ if not isinstance(name, bytes):
+ name = name.encode()
+ index = names.index(name) + 1
+
+ if index == getattr(self, "_last_variation_index", None):
+ # When the same name is set twice in a row,
+ # there is an 'unknown freetype error'
+ # https://savannah.nongnu.org/bugs/?56186
+ return
+ self._last_variation_index = index
+
+ self.font.setvarname(index)
+
+ def get_variation_axes(self) -> list[Axis]:
+ """
+ :returns: A list of the axes in a variation font.
+ :exception OSError: If the font is not a variation font.
+ """
+ axes = self.font.getvaraxes()
+ for axis in axes:
+ if axis["name"]:
+ axis["name"] = axis["name"].replace(b"\x00", b"")
+ return axes
+
+ def set_variation_by_axes(self, axes: list[float]) -> None:
+ """
+ :param axes: A list of values for each axis.
+ :exception OSError: If the font is not a variation font.
+ """
+ self.font.setvaraxes(axes)
+
+
+class TransposedFont(BaseImageFont):
+ """Wrapper for writing rotated or mirrored text"""
+
+ def __init__(
+ self, font: ImageFont | FreeTypeFont, orientation: Image.Transpose | None = None
+ ):
+ """
+ Wrapper that creates a transposed font from any existing font
+ object.
+
+ :param font: A font object.
+ :param orientation: An optional orientation. If given, this should
+ be one of Image.Transpose.FLIP_LEFT_RIGHT, Image.Transpose.FLIP_TOP_BOTTOM,
+ Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_180, or
+ Image.Transpose.ROTATE_270.
+ """
+ self.font = font
+ self.orientation = orientation # any 'transpose' argument, or None
+
+ def getmask(
+ self, text: str | bytes, mode: str = "", *args: Any, **kwargs: Any
+ ) -> Image.core.ImagingCore:
+ im = self.font.getmask(text, mode, *args, **kwargs)
+ if self.orientation is not None:
+ return im.transpose(self.orientation)
+ return im
+
+ def getbbox(
+ self, text: str | bytes | bytearray, *args: Any, **kwargs: Any
+ ) -> tuple[int, int, float, float]:
+ # TransposedFont doesn't support getmask2, move top-left point to (0, 0)
+ # this has no effect on ImageFont and simulates anchor="lt" for FreeTypeFont
+ left, top, right, bottom = self.font.getbbox(text, *args, **kwargs)
+ width = right - left
+ height = bottom - top
+ if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
+ return 0, 0, height, width
+ return 0, 0, width, height
+
+ def getlength(self, text: str | bytes, *args: Any, **kwargs: Any) -> float:
+ if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270):
+ msg = "text length is undefined for text rotated by 90 or 270 degrees"
+ raise ValueError(msg)
+ return self.font.getlength(text, *args, **kwargs)
+
+
+def load(filename: str) -> ImageFont:
+ """
+ Load a font file. This function loads a font object from the given
+ bitmap font file, and returns the corresponding font object. For loading TrueType
+ or OpenType fonts instead, see :py:func:`~PIL.ImageFont.truetype`.
+
+ :param filename: Name of font file.
+ :return: A font object.
+ :exception OSError: If the file could not be read.
+ """
+ f = ImageFont()
+ f._load_pilfont(filename)
+ return f
+
+
+def truetype(
+ font: StrOrBytesPath | BinaryIO,
+ size: float = 10,
+ index: int = 0,
+ encoding: str = "",
+ layout_engine: Layout | None = None,
+) -> FreeTypeFont:
+ """
+ Load a TrueType or OpenType font from a file or file-like object,
+ and create a font object. This function loads a font object from the given
+ file or file-like object, and creates a font object for a font of the given
+ size. For loading bitmap fonts instead, see :py:func:`~PIL.ImageFont.load`
+ and :py:func:`~PIL.ImageFont.load_path`.
+
+ Pillow uses FreeType to open font files. On Windows, be aware that FreeType
+ will keep the file open as long as the FreeTypeFont object exists. Windows
+ limits the number of files that can be open in C at once to 512, so if many
+ fonts are opened simultaneously and that limit is approached, an
+ ``OSError`` may be thrown, reporting that FreeType "cannot open resource".
+ A workaround would be to copy the file(s) into memory, and open that instead.
+
+ This function requires the _imagingft service.
+
+ :param font: A filename or file-like object containing a TrueType font.
+ If the file is not found in this filename, the loader may also
+ search in other directories, such as:
+
+ * The :file:`fonts/` directory on Windows,
+ * :file:`/Library/Fonts/`, :file:`/System/Library/Fonts/`
+ and :file:`~/Library/Fonts/` on macOS.
+ * :file:`~/.local/share/fonts`, :file:`/usr/local/share/fonts`,
+ and :file:`/usr/share/fonts` on Linux; or those specified by
+ the ``XDG_DATA_HOME`` and ``XDG_DATA_DIRS`` environment variables
+ for user-installed and system-wide fonts, respectively.
+
+ :param size: The requested size, in pixels.
+ :param index: Which font face to load (default is first available face).
+ :param encoding: Which font encoding to use (default is Unicode). Possible
+ encodings include (see the FreeType documentation for more
+ information):
+
+ * "unic" (Unicode)
+ * "symb" (Microsoft Symbol)
+ * "ADOB" (Adobe Standard)
+ * "ADBE" (Adobe Expert)
+ * "ADBC" (Adobe Custom)
+ * "armn" (Apple Roman)
+ * "sjis" (Shift JIS)
+ * "gb " (PRC)
+ * "big5"
+ * "wans" (Extended Wansung)
+ * "joha" (Johab)
+ * "lat1" (Latin-1)
+
+ This specifies the character set to use. It does not alter the
+ encoding of any text provided in subsequent operations.
+ :param layout_engine: Which layout engine to use, if available:
+ :attr:`.ImageFont.Layout.BASIC` or :attr:`.ImageFont.Layout.RAQM`.
+ If it is available, Raqm layout will be used by default.
+ Otherwise, basic layout will be used.
+
+ Raqm layout is recommended for all non-English text. If Raqm layout
+ is not required, basic layout will have better performance.
+
+ You can check support for Raqm layout using
+ :py:func:`PIL.features.check_feature` with ``feature="raqm"``.
+
+ .. versionadded:: 4.2.0
+ :return: A font object.
+ :exception OSError: If the file could not be read.
+ :exception ValueError: If the font size is not greater than zero.
+ """
+
+ def freetype(font: StrOrBytesPath | BinaryIO) -> FreeTypeFont:
+ return FreeTypeFont(font, size, index, encoding, layout_engine)
+
+ try:
+ return freetype(font)
+ except OSError:
+ if not is_path(font):
+ raise
+ ttf_filename = os.path.basename(font)
+
+ dirs = []
+ if sys.platform == "win32":
+ # check the windows font repository
+ # NOTE: must use uppercase WINDIR, to work around bugs in
+ # 1.5.2's os.environ.get()
+ windir = os.environ.get("WINDIR")
+ if windir:
+ dirs.append(os.path.join(windir, "fonts"))
+ elif sys.platform in ("linux", "linux2"):
+ data_home = os.environ.get("XDG_DATA_HOME")
+ if not data_home:
+ # The freedesktop spec defines the following default directory for
+ # when XDG_DATA_HOME is unset or empty. This user-level directory
+ # takes precedence over system-level directories.
+ data_home = os.path.expanduser("~/.local/share")
+ xdg_dirs = [data_home]
+
+ data_dirs = os.environ.get("XDG_DATA_DIRS")
+ if not data_dirs:
+ # Similarly, defaults are defined for the system-level directories
+ data_dirs = "/usr/local/share:/usr/share"
+ xdg_dirs += data_dirs.split(":")
+
+ dirs += [os.path.join(xdg_dir, "fonts") for xdg_dir in xdg_dirs]
+ elif sys.platform == "darwin":
+ dirs += [
+ "/Library/Fonts",
+ "/System/Library/Fonts",
+ os.path.expanduser("~/Library/Fonts"),
+ ]
+
+ ext = os.path.splitext(ttf_filename)[1]
+ first_font_with_a_different_extension = None
+ for directory in dirs:
+ for walkroot, walkdir, walkfilenames in os.walk(directory):
+ for walkfilename in walkfilenames:
+ if ext and walkfilename == ttf_filename:
+ return freetype(os.path.join(walkroot, walkfilename))
+ elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename:
+ fontpath = os.path.join(walkroot, walkfilename)
+ if os.path.splitext(fontpath)[1] == ".ttf":
+ return freetype(fontpath)
+ if not ext and first_font_with_a_different_extension is None:
+ first_font_with_a_different_extension = fontpath
+ if first_font_with_a_different_extension:
+ return freetype(first_font_with_a_different_extension)
+ raise
+
+
+def load_path(filename: str | bytes) -> ImageFont:
+ """
+ Load font file. Same as :py:func:`~PIL.ImageFont.load`, but searches for a
+ bitmap font along the Python path.
+
+ :param filename: Name of font file.
+ :return: A font object.
+ :exception OSError: If the file could not be read.
+ """
+ if not isinstance(filename, str):
+ filename = filename.decode("utf-8")
+ for directory in sys.path:
+ try:
+ return load(os.path.join(directory, filename))
+ except OSError: # noqa: PERF203
+ pass
+ msg = f'cannot find font file "{filename}" in sys.path'
+ if os.path.exists(filename):
+ msg += f', did you mean ImageFont.load("{filename}") instead?'
+
+ raise OSError(msg)
+
+
+def load_default_imagefont() -> ImageFont:
+ f = ImageFont()
+ f._load_pilfont_data(
+ # courB08
+ BytesIO(base64.b64decode(b"""
+UElMZm9udAo7Ozs7OzsxMDsKREFUQQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAA//8AAQAAAAAAAAABAAEA
+BgAAAAH/+gADAAAAAQAAAAMABgAGAAAAAf/6AAT//QADAAAABgADAAYAAAAA//kABQABAAYAAAAL
+AAgABgAAAAD/+AAFAAEACwAAABAACQAGAAAAAP/5AAUAAAAQAAAAFQAHAAYAAP////oABQAAABUA
+AAAbAAYABgAAAAH/+QAE//wAGwAAAB4AAwAGAAAAAf/5AAQAAQAeAAAAIQAIAAYAAAAB//kABAAB
+ACEAAAAkAAgABgAAAAD/+QAE//0AJAAAACgABAAGAAAAAP/6AAX//wAoAAAALQAFAAYAAAAB//8A
+BAACAC0AAAAwAAMABgAAAAD//AAF//0AMAAAADUAAQAGAAAAAf//AAMAAAA1AAAANwABAAYAAAAB
+//kABQABADcAAAA7AAgABgAAAAD/+QAFAAAAOwAAAEAABwAGAAAAAP/5AAYAAABAAAAARgAHAAYA
+AAAA//kABQAAAEYAAABLAAcABgAAAAD/+QAFAAAASwAAAFAABwAGAAAAAP/5AAYAAABQAAAAVgAH
+AAYAAAAA//kABQAAAFYAAABbAAcABgAAAAD/+QAFAAAAWwAAAGAABwAGAAAAAP/5AAUAAABgAAAA
+ZQAHAAYAAAAA//kABQAAAGUAAABqAAcABgAAAAD/+QAFAAAAagAAAG8ABwAGAAAAAf/8AAMAAABv
+AAAAcQAEAAYAAAAA//wAAwACAHEAAAB0AAYABgAAAAD/+gAE//8AdAAAAHgABQAGAAAAAP/7AAT/
+/gB4AAAAfAADAAYAAAAB//oABf//AHwAAACAAAUABgAAAAD/+gAFAAAAgAAAAIUABgAGAAAAAP/5
+AAYAAQCFAAAAiwAIAAYAAP////oABgAAAIsAAACSAAYABgAA////+gAFAAAAkgAAAJgABgAGAAAA
+AP/6AAUAAACYAAAAnQAGAAYAAP////oABQAAAJ0AAACjAAYABgAA////+gAFAAAAowAAAKkABgAG
+AAD////6AAUAAACpAAAArwAGAAYAAAAA//oABQAAAK8AAAC0AAYABgAA////+gAGAAAAtAAAALsA
+BgAGAAAAAP/6AAQAAAC7AAAAvwAGAAYAAP////oABQAAAL8AAADFAAYABgAA////+gAGAAAAxQAA
+AMwABgAGAAD////6AAUAAADMAAAA0gAGAAYAAP////oABQAAANIAAADYAAYABgAA////+gAGAAAA
+2AAAAN8ABgAGAAAAAP/6AAUAAADfAAAA5AAGAAYAAP////oABQAAAOQAAADqAAYABgAAAAD/+gAF
+AAEA6gAAAO8ABwAGAAD////6AAYAAADvAAAA9gAGAAYAAAAA//oABQAAAPYAAAD7AAYABgAA////
++gAFAAAA+wAAAQEABgAGAAD////6AAYAAAEBAAABCAAGAAYAAP////oABgAAAQgAAAEPAAYABgAA
+////+gAGAAABDwAAARYABgAGAAAAAP/6AAYAAAEWAAABHAAGAAYAAP////oABgAAARwAAAEjAAYA
+BgAAAAD/+gAFAAABIwAAASgABgAGAAAAAf/5AAQAAQEoAAABKwAIAAYAAAAA//kABAABASsAAAEv
+AAgABgAAAAH/+QAEAAEBLwAAATIACAAGAAAAAP/5AAX//AEyAAABNwADAAYAAAAAAAEABgACATcA
+AAE9AAEABgAAAAH/+QAE//wBPQAAAUAAAwAGAAAAAP/7AAYAAAFAAAABRgAFAAYAAP////kABQAA
+AUYAAAFMAAcABgAAAAD/+wAFAAABTAAAAVEABQAGAAAAAP/5AAYAAAFRAAABVwAHAAYAAAAA//sA
+BQAAAVcAAAFcAAUABgAAAAD/+QAFAAABXAAAAWEABwAGAAAAAP/7AAYAAgFhAAABZwAHAAYAAP//
+//kABQAAAWcAAAFtAAcABgAAAAD/+QAGAAABbQAAAXMABwAGAAAAAP/5AAQAAgFzAAABdwAJAAYA
+AP////kABgAAAXcAAAF+AAcABgAAAAD/+QAGAAABfgAAAYQABwAGAAD////7AAUAAAGEAAABigAF
+AAYAAP////sABQAAAYoAAAGQAAUABgAAAAD/+wAFAAABkAAAAZUABQAGAAD////7AAUAAgGVAAAB
+mwAHAAYAAAAA//sABgACAZsAAAGhAAcABgAAAAD/+wAGAAABoQAAAacABQAGAAAAAP/7AAYAAAGn
+AAABrQAFAAYAAAAA//kABgAAAa0AAAGzAAcABgAA////+wAGAAABswAAAboABQAGAAD////7AAUA
+AAG6AAABwAAFAAYAAP////sABgAAAcAAAAHHAAUABgAAAAD/+wAGAAABxwAAAc0ABQAGAAD////7
+AAYAAgHNAAAB1AAHAAYAAAAA//sABQAAAdQAAAHZAAUABgAAAAH/+QAFAAEB2QAAAd0ACAAGAAAA
+Av/6AAMAAQHdAAAB3gAHAAYAAAAA//kABAABAd4AAAHiAAgABgAAAAD/+wAF//0B4gAAAecAAgAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAB
+//sAAwACAecAAAHpAAcABgAAAAD/+QAFAAEB6QAAAe4ACAAGAAAAAP/5AAYAAAHuAAAB9AAHAAYA
+AAAA//oABf//AfQAAAH5AAUABgAAAAD/+QAGAAAB+QAAAf8ABwAGAAAAAv/5AAMAAgH/AAACAAAJ
+AAYAAAAA//kABQABAgAAAAIFAAgABgAAAAH/+gAE//sCBQAAAggAAQAGAAAAAP/5AAYAAAIIAAAC
+DgAHAAYAAAAB//kABf/+Ag4AAAISAAUABgAA////+wAGAAACEgAAAhkABQAGAAAAAP/7AAX//gIZ
+AAACHgADAAYAAAAA//wABf/9Ah4AAAIjAAEABgAAAAD/+QAHAAACIwAAAioABwAGAAAAAP/6AAT/
++wIqAAACLgABAAYAAAAA//kABP/8Ai4AAAIyAAMABgAAAAD/+gAFAAACMgAAAjcABgAGAAAAAf/5
+AAT//QI3AAACOgAEAAYAAAAB//kABP/9AjoAAAI9AAQABgAAAAL/+QAE//sCPQAAAj8AAgAGAAD/
+///7AAYAAgI/AAACRgAHAAYAAAAA//kABgABAkYAAAJMAAgABgAAAAH//AAD//0CTAAAAk4AAQAG
+AAAAAf//AAQAAgJOAAACUQADAAYAAAAB//kABP/9AlEAAAJUAAQABgAAAAH/+QAF//4CVAAAAlgA
+BQAGAAD////7AAYAAAJYAAACXwAFAAYAAP////kABgAAAl8AAAJmAAcABgAA////+QAGAAACZgAA
+Am0ABwAGAAD////5AAYAAAJtAAACdAAHAAYAAAAA//sABQACAnQAAAJ5AAcABgAA////9wAGAAAC
+eQAAAoAACQAGAAD////3AAYAAAKAAAAChwAJAAYAAP////cABgAAAocAAAKOAAkABgAA////9wAG
+AAACjgAAApUACQAGAAD////4AAYAAAKVAAACnAAIAAYAAP////cABgAAApwAAAKjAAkABgAA////
++gAGAAACowAAAqoABgAGAAAAAP/6AAUAAgKqAAACrwAIAAYAAP////cABQAAAq8AAAK1AAkABgAA
+////9wAFAAACtQAAArsACQAGAAD////3AAUAAAK7AAACwQAJAAYAAP////gABQAAAsEAAALHAAgA
+BgAAAAD/9wAEAAACxwAAAssACQAGAAAAAP/3AAQAAALLAAACzwAJAAYAAAAA//cABAAAAs8AAALT
+AAkABgAAAAD/+AAEAAAC0wAAAtcACAAGAAD////6AAUAAALXAAAC3QAGAAYAAP////cABgAAAt0A
+AALkAAkABgAAAAD/9wAFAAAC5AAAAukACQAGAAAAAP/3AAUAAALpAAAC7gAJAAYAAAAA//cABQAA
+Au4AAALzAAkABgAAAAD/9wAFAAAC8wAAAvgACQAGAAAAAP/4AAUAAAL4AAAC/QAIAAYAAAAA//oA
+Bf//Av0AAAMCAAUABgAA////+gAGAAADAgAAAwkABgAGAAD////3AAYAAAMJAAADEAAJAAYAAP//
+//cABgAAAxAAAAMXAAkABgAA////9wAGAAADFwAAAx4ACQAGAAD////4AAYAAAAAAAoABwASAAYA
+AP////cABgAAAAcACgAOABMABgAA////+gAFAAAADgAKABQAEAAGAAD////6AAYAAAAUAAoAGwAQ
+AAYAAAAA//gABgAAABsACgAhABIABgAAAAD/+AAGAAAAIQAKACcAEgAGAAAAAP/4AAYAAAAnAAoA
+LQASAAYAAAAA//gABgAAAC0ACgAzABIABgAAAAD/+QAGAAAAMwAKADkAEQAGAAAAAP/3AAYAAAA5
+AAoAPwATAAYAAP////sABQAAAD8ACgBFAA8ABgAAAAD/+wAFAAIARQAKAEoAEQAGAAAAAP/4AAUA
+AABKAAoATwASAAYAAAAA//gABQAAAE8ACgBUABIABgAAAAD/+AAFAAAAVAAKAFkAEgAGAAAAAP/5
+AAUAAABZAAoAXgARAAYAAAAA//gABgAAAF4ACgBkABIABgAAAAD/+AAGAAAAZAAKAGoAEgAGAAAA
+AP/4AAYAAABqAAoAcAASAAYAAAAA//kABgAAAHAACgB2ABEABgAAAAD/+AAFAAAAdgAKAHsAEgAG
+AAD////4AAYAAAB7AAoAggASAAYAAAAA//gABQAAAIIACgCHABIABgAAAAD/+AAFAAAAhwAKAIwA
+EgAGAAAAAP/4AAUAAACMAAoAkQASAAYAAAAA//gABQAAAJEACgCWABIABgAAAAD/+QAFAAAAlgAK
+AJsAEQAGAAAAAP/6AAX//wCbAAoAoAAPAAYAAAAA//oABQABAKAACgClABEABgAA////+AAGAAAA
+pQAKAKwAEgAGAAD////4AAYAAACsAAoAswASAAYAAP////gABgAAALMACgC6ABIABgAA////+QAG
+AAAAugAKAMEAEQAGAAD////4AAYAAgDBAAoAyAAUAAYAAP////kABQACAMgACgDOABMABgAA////
++QAGAAIAzgAKANUAEw==
+""")),
+ Image.open(BytesIO(base64.b64decode(b"""
+iVBORw0KGgoAAAANSUhEUgAAAx4AAAAUAQAAAAArMtZoAAAEwElEQVR4nABlAJr/AHVE4czCI/4u
+Mc4b7vuds/xzjz5/3/7u/n9vMe7vnfH/9++vPn/xyf5zhxzjt8GHw8+2d83u8x27199/nxuQ6Od9
+M43/5z2I+9n9ZtmDBwMQECDRQw/eQIQohJXxpBCNVE6QCCAAAAD//wBlAJr/AgALyj1t/wINwq0g
+LeNZUworuN1cjTPIzrTX6ofHWeo3v336qPzfEwRmBnHTtf95/fglZK5N0PDgfRTslpGBvz7LFc4F
+IUXBWQGjQ5MGCx34EDFPwXiY4YbYxavpnhHFrk14CDAAAAD//wBlAJr/AgKqRooH2gAgPeggvUAA
+Bu2WfgPoAwzRAABAAAAAAACQgLz/3Uv4Gv+gX7BJgDeeGP6AAAD1NMDzKHD7ANWr3loYbxsAD791
+NAADfcoIDyP44K/jv4Y63/Z+t98Ovt+ub4T48LAAAAD//wBlAJr/AuplMlADJAAAAGuAphWpqhMx
+in0A/fRvAYBABPgBwBUgABBQ/sYAyv9g0bCHgOLoGAAAAAAAREAAwI7nr0ArYpow7aX8//9LaP/9
+SjdavWA8ePHeBIKB//81/83ndznOaXx379wAAAD//wBlAJr/AqDxW+D3AABAAbUh/QMnbQag/gAY
+AYDAAACgtgD/gOqAAAB5IA/8AAAk+n9w0AAA8AAAmFRJuPo27ciC0cD5oeW4E7KA/wD3ECMAn2tt
+y8PgwH8AfAxFzC0JzeAMtratAsC/ffwAAAD//wBlAJr/BGKAyCAA4AAAAvgeYTAwHd1kmQF5chkG
+ABoMIHcL5xVpTfQbUqzlAAAErwAQBgAAEOClA5D9il08AEh/tUzdCBsXkbgACED+woQg8Si9VeqY
+lODCn7lmF6NhnAEYgAAA/NMIAAAAAAD//2JgjLZgVGBg5Pv/Tvpc8hwGBjYGJADjHDrAwPzAjv/H
+/Wf3PzCwtzcwHmBgYGcwbZz8wHaCAQMDOwMDQ8MCBgYOC3W7mp+f0w+wHOYxO3OG+e376hsMZjk3
+AAAAAP//YmCMY2A4wMAIN5e5gQETPD6AZisDAwMDgzSDAAPjByiHcQMDAwMDg1nOze1lByRu5/47
+c4859311AYNZzg0AAAAA//9iYGDBYihOIIMuwIjGL39/fwffA8b//xv/P2BPtzzHwCBjUQAAAAD/
+/yLFBrIBAAAA//9i1HhcwdhizX7u8NZNzyLbvT97bfrMf/QHI8evOwcSqGUJAAAA//9iYBB81iSw
+pEE170Qrg5MIYydHqwdDQRMrAwcVrQAAAAD//2J4x7j9AAMDn8Q/BgYLBoaiAwwMjPdvMDBYM1Tv
+oJodAAAAAP//Yqo/83+dxePWlxl3npsel9lvLfPcqlE9725C+acfVLMEAAAA//9i+s9gwCoaaGMR
+evta/58PTEWzr21hufPjA8N+qlnBwAAAAAD//2JiWLci5v1+HmFXDqcnULE/MxgYGBj+f6CaJQAA
+AAD//2Ji2FrkY3iYpYC5qDeGgeEMAwPDvwQBBoYvcTwOVLMEAAAA//9isDBgkP///0EOg9z35v//
+Gc/eeW7BwPj5+QGZhANUswMAAAD//2JgqGBgYGBgqEMXlvhMPUsAAAAA//8iYDd1AAAAAP//AwDR
+w7IkEbzhVQAAAABJRU5ErkJggg==
+"""))),
+ )
+ return f
+
+
+def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
+ """If FreeType support is available, load a version of Aileron Regular,
+ https://dotcolon.net/fonts/aileron, with a more limited character set.
+
+ Otherwise, load a "better than nothing" font.
+
+ .. versionadded:: 1.1.4
+
+ :param size: The font size of Aileron Regular.
+
+ .. versionadded:: 10.1.0
+
+ :return: A font object.
+ """
+ if isinstance(core, ModuleType) or size is not None:
+ return truetype(
+ BytesIO(base64.b64decode(b"""
+AAEAAAAPAIAAAwBwRkZUTYwDlUAAADFoAAAAHEdERUYAqADnAAAo8AAAACRHUE9ThhmITwAAKfgAA
+AduR1NVQnHxefoAACkUAAAA4k9TLzJovoHLAAABeAAAAGBjbWFw5lFQMQAAA6gAAAGqZ2FzcP//AA
+MAACjoAAAACGdseWYmRXoPAAAGQAAAHfhoZWFkE18ayQAAAPwAAAA2aGhlYQboArEAAAE0AAAAJGh
+tdHjjERZ8AAAB2AAAAdBsb2NhuOexrgAABVQAAADqbWF4cAC7AEYAAAFYAAAAIG5hbWUr+h5lAAAk
+OAAAA6Jwb3N0D3oPTQAAJ9wAAAEKAAEAAAABGhxJDqIhXw889QALA+gAAAAA0Bqf2QAAAADhCh2h/
+2r/LgOxAyAAAAAIAAIAAAAAAAAAAQAAA8r/GgAAA7j/av9qA7EAAQAAAAAAAAAAAAAAAAAAAHQAAQ
+AAAHQAQwAFAAAAAAACAAAAAQABAAAAQAAAAAAAAAADAfoBkAAFAAgCigJYAAAASwKKAlgAAAFeADI
+BPgAAAAAFAAAAAAAAAAAAAAcAAAAAAAAAAAAAAABVS1dOAEAAIPsCAwL/GgDIA8oA5iAAAJMAAAAA
+AhICsgAAACAAAwH0AAAAAAAAAU0AAADYAAAA8gA5AVMAVgJEAEYCRAA1AuQAKQKOAEAAsAArATsAZ
+AE7AB4CMABVAkQAUADc/+EBEgAgANwAJQEv//sCRAApAkQAggJEADwCRAAtAkQAIQJEADkCRAArAk
+QAMgJEACwCRAAxANwAJQDc/+ECRABnAkQAUAJEAEQB8wAjA1QANgJ/AB0CcwBkArsALwLFAGQCSwB
+kAjcAZALGAC8C2gBkAQgAZAIgADcCYQBkAj8AZANiAGQCzgBkAuEALwJWAGQC3QAvAmsAZAJJADQC
+ZAAiAqoAXgJuACADuAAaAnEAGQJFABMCTwAuATMAYgEv//sBJwAiAkQAUAH0ADIBLAApAhMAJAJjA
+EoCEQAeAmcAHgIlAB4BIgAVAmcAHgJRAEoA7gA+AOn/8wIKAEoA9wBGA1cASgJRAEoCSgAeAmMASg
+JnAB4BSgBKAcsAGAE5ABQCUABCAgIAAQMRAAEB4v/6AgEAAQHOABQBLwBAAPoAYAEvACECRABNA0Y
+AJAItAHgBKgAcAkQAUAEsAHQAygAgAi0AOQD3ADYA9wAWAaEANgGhABYCbAAlAYMAeAGDADkA6/9q
+AhsAFAIKABUB/QAVAAAAAwAAAAMAAAAcAAEAAAAAAKQAAwABAAAAHAAEAIgAAAAeABAAAwAOAH4Aq
+QCrALEAtAC3ALsgGSAdICYgOiBEISL7Av//AAAAIACpAKsAsAC0ALcAuyAYIBwgJiA5IEQhIvsB//
+//4/+5/7j/tP+y/7D/reBR4E/gR+A14CzfTwVxAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAEGAAABAAAAAAAAAAECAAAAAgAAAAAAAAAAAAAAAAAAAAEAAAMEBQYHCAkKCwwNDg8QERIT
+FBUWFxgZGhscHR4fICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVGR0hJSktMT
+U5PUFFSU1RVVldYWVpbXF1eX2BhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAA
+AAAAAAYnFmAAAAAABlAAAAAAAAAAAAAAAAAAAAAAAAAAAAY2htAAAAAAAAAABrbGlqAAAAAHAAbm9
+ycwBnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACYAJgAmAD4AUgCCAMoBCgFO
+AVwBcgGIAaYBvAHKAdYB6AH2AgwCIAJKAogCpgLWAw4DIgNkA5wDugPUA+gD/AQQBEYEogS8BPoFJ
+gVSBWoFgAWwBcoF1gX6BhQGJAZMBmgGiga0BuIHGgdUB2YHkAeiB8AH3AfyCAoIHAgqCDoITghcCG
+oIogjSCPoJKglYCXwJwgnqCgIKKApACl4Klgq8CtwLDAs8C1YLjAuyC9oL7gwMDCYMSAxgDKAMrAz
+qDQoNTA1mDYQNoA2uDcAN2g3oDfYODA4iDkoOXA5sDnoOnA7EDvwAAAAFAAAAAAH0ArwAAwAGAAkA
+DAAPAAAxESERAxMhExcRASELARETAfT6qv6syKr+jgFUqsiqArz9RAGLAP/+1P8B/v3VAP8BLP4CA
+P8AAgA5//IAuQKyAAMACwAANyMDMwIyFhQGIiY0oE4MZk84JCQ4JLQB/v3AJDgkJDgAAgBWAeUBPA
+LfAAMABwAAEyMnMxcjJzOmRgpagkYKWgHl+vr6AAAAAAIARgAAAf4CsgAbAB8AAAEHMxUjByM3Iwc
+jNyM1MzcjNTM3MwczNzMHMxUrAQczAZgdZXEvOi9bLzovWmYdZXEvOi9bLzovWp9bHlsBn4w429vb
+2ziMONvb29s4jAAAAAMANf+mAg4DDAAfACYALAAAJRQGBxUjNS4BJzMeARcRLgE0Njc1MxUeARcjJ
+icVHgEBFBYXNQ4BExU+ATU0Ag5xWDpgcgRcBz41Xl9oVTpVYwpcC1ttXP6cLTQuM5szOrVRZwlOTQ
+ZqVzZECAEAGlukZAlOTQdrUG8O7iNlAQgxNhDlCDj+8/YGOjReAAAAAAUAKf/yArsCvAAHAAsAFQA
+dACcAABIyFhQGIiY0EyMBMwQiBhUUFjI2NTQSMhYUBiImNDYiBhUUFjI2NTR5iFBQiFCVVwHAV/5c
+OiMjOiPmiFBQiFCxOiMjOiMCvFaSVlaS/ZoCsjIzMC80NC8w/uNWklZWkhozMC80NC8wAAAAAgBA/
+/ICbgLAACIALgAAARUjEQYjIiY1NDY3LgE1NDYzMhcVJiMiBhUUFhcWOwE1MxUFFBYzMjc1IyIHDg
+ECbmBcYYOOVkg7R4hsQjY4Q0RNRD4SLDxW/pJUXzksPCkUUk0BgUb+zBVUZ0BkDw5RO1huCkULQzp
+COAMBcHDHRz0J/AIHRQAAAAEAKwHlAIUC3wADAAATIycze0YKWgHl+gAAAAABAGT/sAEXAwwACQAA
+EzMGEBcjLgE0Nt06dXU6OUBAAwzG/jDGVePs4wAAAAEAHv+wANEDDAAJAAATMx4BFAYHIzYQHjo5Q
+EA5OnUDDFXj7ONVxgHQAAAAAQBVAFIB2wHbAA4AAAE3FwcXBycHJzcnNxcnMwEtmxOfcTJjYzJxnx
+ObCj4BKD07KYolmZkliik7PbMAAQBQAFUB9AIlAAsAAAEjFSM1IzUzNTMVMwH0tTq1tTq1AR/Kyjj
+OzgAAAAAB/+H/iACMAGQABAAANwcjNzOMWlFOXVrS3AAAAQAgAP8A8gE3AAMAABMjNTPy0tIA/zgA
+AQAl//IApQByAAcAADYyFhQGIiY0STgkJDgkciQ4JCQ4AAAAAf/7/+IBNALQAAMAABcjEzM5Pvs+H
+gLuAAAAAAIAKf/yAhsCwAADAAcAABIgECA2IBAgKQHy/g5gATL+zgLA/TJEAkYAAAAAAQCCAAABlg
+KyAAgAAAERIxEHNTc2MwGWVr6SIygCsv1OAldxW1sWAAEAPAAAAg4CwAAZAAA3IRUhNRM+ATU0JiM
+iDwEjNz4BMzIWFRQGB7kBUv4x+kI2QTt+EAFWAQp8aGVtSl5GRjEA/0RVLzlLmAoKa3FsUkNxXQAA
+AAEALf/yAhYCwAAqAAABHgEVFAYjIi8BMxceATMyNjU0KwE1MzI2NTQmIyIGDwEjNz4BMzIWFRQGA
+YxBSZJo2RUBVgEHV0JBUaQREUBUQzc5TQcBVgEKfGhfcEMBbxJbQl1x0AoKRkZHPn9GSD80QUVCCg
+pfbGBPOlgAAAACACEAAAIkArIACgAPAAAlIxUjNSE1ATMRMyMRBg8BAiRXVv6qAVZWV60dHLCurq4
+rAdn+QgFLMibzAAABADn/8gIZArIAHQAAATIWFRQGIyIvATMXFjMyNjU0JiMiByMTIRUhBzc2ATNv
+d5Fl1RQBVgIad0VSTkVhL1IwAYj+vh8rMAHHgGdtgcUKCoFXTU5bYgGRRvAuHQAAAAACACv/8gITA
+sAAFwAjAAABMhYVFAYjIhE0NjMyFh8BIycmIyIDNzYTMjY1NCYjIgYVFBYBLmp7imr0l3RZdAgBXA
+IYZ5wKJzU6QVNJSz5SUAHSgWltiQFGxcNlVQoKdv7sPiz+ZF1LTmJbU0lhAAAAAQAyAAACGgKyAAY
+AAAEVASMBITUCGv6oXAFL/oECsij9dgJsRgAAAAMALP/xAhgCwAAWACAALAAAAR4BFRQGIyImNTQ2
+Ny4BNTQ2MhYVFAYmIgYVFBYyNjU0AzI2NTQmIyIGFRQWAZQ5S5BmbIpPOjA7ecp5P2F8Q0J8RIVJS
+0pLTEtOAW0TXTxpZ2ZqPF0SE1A3VWVlVTdQ/UU0N0RENzT9/ko+Ok1NOj1LAAIAMf/yAhkCwAAXAC
+MAAAEyERQGIyImLwEzFxYzMhMHBiMiJjU0NhMyNjU0JiMiBhUUFgEl9Jd0WXQIAVwCGGecCic1SWp
+7imo+UlBAQVNJAsD+usXDZVUKCnYBFD4sgWltif5kW1NJYV1LTmIAAAACACX/8gClAiAABwAPAAAS
+MhYUBiImNBIyFhQGIiY0STgkJDgkJDgkJDgkAiAkOCQkOP52JDgkJDgAAAAC/+H/iAClAiAABwAMA
+AASMhYUBiImNBMHIzczSTgkJDgkaFpSTl4CICQ4JCQ4/mba5gAAAQBnAB4B+AH0AAYAAAENARUlNS
+UB+P6qAVb+bwGRAbCmpkbJRMkAAAIAUAC7AfQBuwADAAcAAAEhNSERITUhAfT+XAGk/lwBpAGDOP8
+AOAABAEQAHgHVAfQABgAAARUFNS0BNQHV/m8BVv6qAStEyUSmpkYAAAAAAgAj//IB1ALAABgAIAAA
+ATIWFRQHDgEHIz4BNz4BNTQmIyIGByM+ARIyFhQGIiY0AQRibmktIAJWBSEqNig+NTlHBFoDezQ4J
+CQ4JALAZ1BjaS03JS1DMD5LLDQ/SUVgcv2yJDgkJDgAAAAAAgA2/5gDFgKYADYAQgAAAQMGFRQzMj
+Y1NCYjIg4CFRQWMzI2NxcGIyImNTQ+AjMyFhUUBiMiJwcGIyImNTQ2MzIfATcHNzYmIyIGFRQzMjY
+Cej8EJjJJlnBAfGQ+oHtAhjUYg5OPx0h2k06Os3xRWQsVLjY5VHtdPBwJETcJDyUoOkZEJz8B0f74
+EQ8kZl6EkTFZjVOLlyknMVm1pmCiaTq4lX6CSCknTVRmmR8wPdYnQzxuSWVGAAIAHQAAAncCsgAHA
+AoAACUjByMTMxMjATMDAcj+UVz4dO5d/sjPZPT0ArL9TgE6ATQAAAADAGQAAAJMArIAEAAbACcAAA
+EeARUUBgcGKwERMzIXFhUUJRUzMjc2NTQnJiMTPgE1NCcmKwEVMzIBvkdHZkwiNt7LOSGq/oeFHBt
+hahIlSTM+cB8Yj5UWAW8QT0VYYgwFArIEF5Fv1eMED2NfDAL93AU+N24PBP0AAAAAAQAv//ICjwLA
+ABsAAAEyFh8BIycmIyIGFRQWMzI/ATMHDgEjIiY1NDYBdX+PCwFWAiKiaHx5ZaIiAlYBCpWBk6a0A
+sCAagoKpqN/gaOmCgplhcicn8sAAAIAZAAAAp8CsgAMABkAAAEeARUUBgcGKwERMzITPgE1NCYnJi
+sBETMyAY59lJp8IzXN0jUVWmdjWRs5d3I4Aq4QqJWUug8EArL9mQ+PeHGHDgX92gAAAAABAGQAAAI
+vArIACwAAJRUhESEVIRUhFSEVAi/+NQHB/pUBTf6zRkYCskbwRvAAAAABAGQAAAIlArIACQAAExUh
+FSERIxEhFboBQ/69VgHBAmzwRv7KArJGAAAAAAEAL//yAo8CwAAfAAABMxEjNQcGIyImNTQ2MzIWH
+wEjJyYjIgYVFBYzMjY1IwGP90wfPnWTprSSf48LAVYCIqJofHllVG+hAU3+s3hARsicn8uAagoKpq
+N/gaN1XAAAAAEAZAAAAowCsgALAAABESMRIREjETMRIRECjFb+hFZWAXwCsv1OAS7+0gKy/sQBPAA
+AAAABAGQAAAC6ArIAAwAAMyMRM7pWVgKyAAABADf/8gHoArIAEwAAAREUBw4BIyImLwEzFxYzMjc2
+NREB6AIFcGpgbQIBVgIHfXQKAQKy/lYxIltob2EpKYyEFD0BpwAAAAABAGQAAAJ0ArIACwAACQEjA
+wcVIxEzEQEzATsBJ3ntQlZWAVVlAWH+nwEnR+ACsv6RAW8AAQBkAAACLwKyAAUAACUVIREzEQIv/j
+VWRkYCsv2UAAABAGQAAAMUArIAFAAAAREjETQ3BgcDIwMmJxYVESMRMxsBAxRWAiMxemx8NxsCVo7
+MywKy/U4BY7ZLco7+nAFmoFxLtP6dArL9lwJpAAAAAAEAZAAAAoACsgANAAAhIwEWFREjETMBJjUR
+MwKAhP67A1aEAUUDVAJeeov+pwKy/aJ5jAFZAAAAAgAv//ICuwLAAAkAEwAAEiAWFRQGICY1NBIyN
+jU0JiIGFRTbATSsrP7MrNrYenrYegLAxaKhxsahov47nIeIm5uIhwACAGQAAAJHArIADgAYAAABHg
+EVFAYHBisBESMRMzITNjQnJisBETMyAZRUX2VOHzuAVtY7GlxcGDWIiDUCrgtnVlVpCgT+5gKy/rU
+V1BUF/vgAAAACAC//zAK9AsAAEgAcAAAlFhcHJiMiBwYjIiY1NDYgFhUUJRQWMjY1NCYiBgI9PUMx
+UDcfKh8omqysATSs/dR62Hp62HpICTg7NgkHxqGixcWitbWHnJyHiJubAAIAZAAAAlgCsgAXACMAA
+CUWFyMmJyYnJisBESMRMzIXHgEVFAYHFiUzMjc+ATU0JyYrAQIqDCJfGQwNWhAhglbiOx9QXEY1Tv
+6bhDATMj1lGSyMtYgtOXR0BwH+1wKyBApbU0BSESRAAgVAOGoQBAABADT/8gIoAsAAJQAAATIWFyM
+uASMiBhUUFhceARUUBiMiJiczHgEzMjY1NCYnLgE1NDYBOmd2ClwGS0E6SUNRdW+HZnKKC1wPWkQ9
+Uk1cZGuEAsBwXUJHNjQ3OhIbZVZZbm5kREo+NT5DFRdYUFdrAAAAAAEAIgAAAmQCsgAHAAABIxEjE
+SM1IQJk9lb2AkICbP2UAmxGAAEAXv/yAmQCsgAXAAABERQHDgEiJicmNREzERQXHgEyNjc2NRECZA
+IIgfCBCAJWAgZYmlgGAgKy/k0qFFxzc1wUKgGz/lUrEkRQUEQSKwGrAAAAAAEAIAAAAnoCsgAGAAA
+hIwMzGwEzAYJ07l3N1FwCsv2PAnEAAAEAGgAAA7ECsgAMAAABAyMLASMDMxsBMxsBA7HAcZyicrZi
+kaB0nJkCsv1OAlP9rQKy/ZsCW/2kAmYAAAEAGQAAAm8CsgALAAAhCwEjEwMzGwEzAxMCCsrEY/bkY
+re+Y/D6AST+3AFcAVb+5gEa/q3+oQAAAQATAAACUQKyAAgAAAERIxEDMxsBMwFdVvRjwLphARD+8A
+EQAaL+sQFPAAABAC4AAAI5ArIACQAAJRUhNQEhNSEVAQI5/fUBof57Aen+YUZGQgIqRkX92QAAAAA
+BAGL/sAEFAwwABwAAARUjETMVIxEBBWlpowMMOP0UOANcAAAB//v/4gE0AtAAAwAABSMDMwE0Pvs+
+HgLuAAAAAQAi/7AAxQMMAAcAABcjNTMRIzUzxaNpaaNQOALsOAABAFAA1wH0AmgABgAAJQsBIxMzE
+wGwjY1GsESw1wFZ/qcBkf5vAAAAAQAy/6oBwv/iAAMAAAUhNSEBwv5wAZBWOAAAAAEAKQJEALYCsg
+ADAAATIycztjhVUAJEbgAAAAACACT/8gHQAiAAHQAlAAAhJwcGIyImNTQ2OwE1NCcmIyIHIz4BMzI
+XFh0BFBcnMjY9ASYVFAF6CR0wVUtgkJoiAgdgaQlaBm1Zrg4DCuQ9R+5MOSFQR1tbDiwUUXBUXowf
+J8c9SjRORzYSgVwAAAAAAgBK//ICRQLfABEAHgAAATIWFRQGIyImLwEVIxEzETc2EzI2NTQmIyIGH
+QEUFgFUcYCVbiNJEyNWVigySElcU01JXmECIJd4i5QTEDRJAt/+3jkq/hRuZV55ZWsdX14AAQAe//
+IB9wIgABgAAAEyFhcjJiMiBhUUFjMyNjczDgEjIiY1NDYBF152DFocbEJXU0A1Rw1aE3pbaoKQAiB
+oWH5qZm1tPDlaXYuLgZcAAAACAB7/8gIZAt8AEQAeAAABESM1BwYjIiY1NDYzMhYfAREDMjY9ATQm
+IyIGFRQWAhlWKDJacYCVbiNJEyOnSV5hQUlcUwLf/SFVOSqXeIuUExA0ARb9VWVrHV9ebmVeeQACA
+B7/8gH9AiAAFQAbAAABFAchHgEzMjY3Mw4BIyImNTQ2MzIWJyIGByEmAf0C/oAGUkA1SwlaD4FXbI
+WObmt45UBVBwEqDQEYFhNjWD84W16Oh3+akU9aU60AAAEAFQAAARoC8gAWAAATBh0BMxUjESMRIzU
+zNTQ3PgEzMhcVJqcDbW1WOTkDB0k8Hx5oAngVITRC/jQBzEIsJRs5PwVHEwAAAAIAHv8uAhkCIAAi
+AC8AAAERFAcOASMiLwEzFx4BMzI2NzY9AQcGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZAQSEd
+NwRAVcBBU5DTlUDASgyWnGAlW4jSRMjp0leYUFJXFMCEv5wSh1zeq8KCTI8VU0ZIQk5Kpd4i5QTED
+RJ/iJlax1fXm5lXnkAAQBKAAACCgLkABcAAAEWFREjETQnLgEHDgEdASMRMxE3NjMyFgIIAlYCBDs
+6RVRWViE5UVViAYUbQP7WASQxGzI7AQJyf+kC5P7TPSxUAAACAD4AAACsAsAABwALAAASMhYUBiIm
+NBMjETNeLiAgLiBiVlYCwCAuICAu/WACEgAC//P/LgCnAsAABwAVAAASMhYUBiImNBcRFAcGIyInN
+RY3NjURWS4gIC4gYgMLcRwNSgYCAsAgLiAgLo79wCUbZAJGBzMOHgJEAAAAAQBKAAACCALfAAsAAC
+EnBxUjETMREzMHEwGTwTJWVvdu9/rgN6kC3/4oAQv6/ugAAQBG//wA3gLfAA8AABMRFBceATcVBiM
+iJicmNRGcAQIcIxkkKi4CAQLf/bkhERoSBD4EJC8SNAJKAAAAAQBKAAADEAIgACQAAAEWFREjETQn
+JiMiFREjETQnJiMiFREjETMVNzYzMhYXNzYzMhYDCwVWBAxedFYEDF50VlYiJko7ThAvJkpEVAGfI
+jn+vAEcQyRZ1v76ARxDJFnW/voCEk08HzYtRB9HAAAAAAEASgAAAgoCIAAWAAABFhURIxE0JyYjIg
+YdASMRMxU3NjMyFgIIAlYCCXBEVVZWITlRVWIBhRtA/tYBJDEbbHR/6QISWz0sVAAAAAACAB7/8gI
+sAiAABwARAAASIBYUBiAmNBIyNjU0JiIGFRSlAQCHh/8Ah7ieWlqeWgIgn/Cfn/D+s3ZfYHV1YF8A
+AgBK/zwCRQIgABEAHgAAATIWFRQGIyImLwERIxEzFTc2EzI2NTQmIyIGHQEUFgFUcYCVbiNJEyNWV
+igySElcU01JXmECIJd4i5QTEDT+8wLWVTkq/hRuZV55ZWsdX14AAgAe/zwCGQIgABEAHgAAAREjEQ
+cGIyImNTQ2MzIWHwE1AzI2PQE0JiMiBhUUFgIZVigyWnGAlW4jSRMjp0leYUFJXFMCEv0qARk5Kpd
+4i5QTEDRJ/iJlax1fXm5lXnkAAQBKAAABPgIeAA0AAAEyFxUmBhURIxEzFTc2ARoWDkdXVlYwIwIe
+B0EFVlf+0gISU0cYAAEAGP/yAa0CIAAjAAATMhYXIyYjIgYVFBYXHgEVFAYjIiYnMxYzMjY1NCYnL
+gE1NDbkV2MJWhNdKy04PF1XbVhWbgxaE2ktOjlEUllkAiBaS2MrJCUoEBlPQkhOVFZoKCUmLhIWSE
+BIUwAAAAEAFP/4ARQCiQAXAAATERQXHgE3FQYjIiYnJjURIzUzNTMVMxWxAQMmMx8qMjMEAUdHVmM
+BzP7PGw4mFgY/BSwxDjQBNUJ7e0IAAAABAEL/8gICAhIAFwAAAREjNQcGIyImJyY1ETMRFBceATMy
+Nj0BAgJWITlRT2EKBVYEBkA1RFECEv3uWj4qTToiOQE+/tIlJC43c4DpAAAAAAEAAQAAAfwCEgAGA
+AABAyMDMxsBAfzJaclfop8CEv3uAhL+LQHTAAABAAEAAAMLAhIADAAAAQMjCwEjAzMbATMbAQMLqW
+Z2dmapY3t0a3Z7AhL97gG+/kICEv5AAcD+QwG9AAAB//oAAAHWAhIACwAAARMjJwcjEwMzFzczARq
+8ZIuKY763ZoWFYwEO/vLV1QEMAQbNzQAAAQAB/y4B+wISABEAAAEDDgEjIic1FjMyNj8BAzMbAQH7
+2iFZQB8NDRIpNhQH02GenQIS/cFVUAJGASozEwIt/i4B0gABABQAAAGxAg4ACQAAJRUhNQEhNSEVA
+QGx/mMBNP7iAYL+zkREQgGIREX+ewAAAAABAED/sAEOAwwALAAAASMiBhUUFxYVFAYHHgEVFAcGFR
+QWOwEVIyImNTQ3NjU0JzU2NTQnJjU0NjsBAQ4MKiMLDS4pKS4NCyMqDAtERAwLUlILDERECwLUGBk
+WTlsgKzUFBTcrIFtOFhkYOC87GFVMIkUIOAhFIkxVGDsvAAAAAAEAYP84AJoDIAADAAAXIxEzmjo6
+yAPoAAEAIf+wAO8DDAAsAAATFQYVFBcWFRQGKwE1MzI2NTQnJjU0NjcuATU0NzY1NCYrATUzMhYVF
+AcGFRTvUgsMREQLDCojCw0uKSkuDQsjKgwLREQMCwF6OAhFIkxVGDsvOBgZFk5bICs1BQU3KyBbTh
+YZGDgvOxhVTCJFAAABAE0A3wH2AWQAEwAAATMUIyImJyYjIhUjNDMyFhcWMzIBvjhuGywtQR0xOG4
+bLC1BHTEBZIURGCNMhREYIwAAAwAk/94DIgLoAAcAEQApAAAAIBYQBiAmECQgBhUUFiA2NTQlMhYX
+IyYjIgYUFjMyNjczDgEjIiY1NDYBAQFE3d3+vN0CB/7wubkBELn+xVBnD1wSWDo+QTcqOQZcEmZWX
+HN2Aujg/rbg4AFKpr+Mjb6+jYxbWEldV5ZZNShLVn5na34AAgB4AFIB9AGeAAUACwAAAQcXIyc3Mw
+cXIyc3AUqJiUmJifOJiUmJiQGepqampqampqYAAAIAHAHSAQ4CwAAHAA8AABIyFhQGIiY0NiIGFBY
+yNjRgakREakSTNCEhNCECwEJqQkJqCiM4IyM4AAAAAAIAUAAAAfQCCwALAA8AAAEzFSMVIzUjNTM1
+MxMhNSEBP7W1OrW1OrX+XAGkAVs4tLQ4sP31OAAAAQB0AkQBAQKyAAMAABMjNzOsOD1QAkRuAAAAA
+AEAIADsAKoBdgAHAAASMhYUBiImNEg6KCg6KAF2KDooKDoAAAIAOQBSAbUBngAFAAsAACUHIzcnMw
+UHIzcnMwELiUmJiUkBM4lJiYlJ+KampqampqYAAAABADYB5QDhAt8ABAAAEzczByM2Xk1OXQHv8Po
+AAQAWAeUAwQLfAAQAABMHIzczwV5NTl0C1fD6AAIANgHlAYsC3wAEAAkAABM3MwcjPwEzByM2Xk1O
+XapeTU5dAe/w+grw+gAAAgAWAeUBawLfAAQACQAAEwcjNzMXByM3M8FeTU5dql5NTl0C1fD6CvD6A
+AADACX/8gI1AHIABwAPABcAADYyFhQGIiY0NjIWFAYiJjQ2MhYUBiImNEk4JCQ4JOw4JCQ4JOw4JC
+Q4JHIkOCQkOCQkOCQkOCQkOCQkOAAAAAEAeABSAUoBngAFAAABBxcjJzcBSomJSYmJAZ6mpqamAAA
+AAAEAOQBSAQsBngAFAAAlByM3JzMBC4lJiYlJ+KampgAAAf9qAAABgQKyAAMAACsBATM/VwHAVwKy
+AAAAAAIAFAHIAdwClAAHABQAABMVIxUjNSM1BRUjNwcjJxcjNTMXN9pKMkoByDICKzQqATJLKysCl
+CmjoykBy46KiY3Lm5sAAQAVAAABvALyABgAAAERIxEjESMRIzUzNTQ3NjMyFxUmBgcGHQEBvFbCVj
+k5AxHHHx5iVgcDAg798gHM/jQBzEIOJRuWBUcIJDAVIRYAAAABABX//AHkAvIAJQAAJR4BNxUGIyI
+mJyY1ESYjIgcGHQEzFSMRIxEjNTM1NDc2MzIXERQBowIcIxkkKi4CAR4nXgwDbW1WLy8DEbNdOmYa
+EQQ/BCQvEjQCFQZWFSEWQv40AcxCDiUblhP9uSEAAAAAAAAWAQ4AAQAAAAAAAAATACgAAQAAAAAAA
+QAHAEwAAQAAAAAAAgAHAGQAAQAAAAAAAwAaAKIAAQAAAAAABAAHAM0AAQAAAAAABQA8AU8AAQAAAA
+AABgAPAawAAQAAAAAACAALAdQAAQAAAAAACQALAfgAAQAAAAAACwAXAjQAAQAAAAAADAAXAnwAAwA
+BBAkAAAAmAAAAAwABBAkAAQAOADwAAwABBAkAAgAOAFQAAwABBAkAAwA0AGwAAwABBAkABAAOAL0A
+AwABBAkABQB4ANUAAwABBAkABgAeAYwAAwABBAkACAAWAbwAAwABBAkACQAWAeAAAwABBAkACwAuA
+gQAAwABBAkADAAuAkwATgBvACAAUgBpAGcAaAB0AHMAIABSAGUAcwBlAHIAdgBlAGQALgAATm8gUm
+lnaHRzIFJlc2VydmVkLgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAUgBlAGcAdQBsAGEAcgAAUmV
+ndWxhcgAAMQAuADEAMAAyADsAVQBLAFcATgA7AEEAaQBsAGUAcgBvAG4ALQBSAGUAZwB1AGwAYQBy
+AAAxLjEwMjtVS1dOO0FpbGVyb24tUmVndWxhcgAAQQBpAGwAZQByAG8AbgAAQWlsZXJvbgAAVgBlA
+HIAcwBpAG8AbgAgADEALgAxADAAMgA7AFAAUwAgADAAMAAxAC4AMQAwADIAOwBoAG8AdABjAG8Abg
+B2ACAAMQAuADAALgA3ADAAOwBtAGEAawBlAG8AdABmAC4AbABpAGIAMgAuADUALgA1ADgAMwAyADk
+AAFZlcnNpb24gMS4xMDI7UFMgMDAxLjEwMjtob3Rjb252IDEuMC43MDttYWtlb3RmLmxpYjIuNS41
+ODMyOQAAQQBpAGwAZQByAG8AbgAtAFIAZQBnAHUAbABhAHIAAEFpbGVyb24tUmVndWxhcgAAUwBvA
+HIAYQAgAFMAYQBnAGEAbgBvAABTb3JhIFNhZ2FubwAAUwBvAHIAYQAgAFMAYQBnAGEAbgBvAABTb3
+JhIFNhZ2FubwAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBsAG8AbgAuAG4AZQB0AAB
+odHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGQAbwB0AGMAbwBs
+AG8AbgAuAG4AZQB0AABodHRwOi8vd3d3LmRvdGNvbG9uLm5ldAAAAAACAAAAAAAA/4MAMgAAAAAAA
+AAAAAAAAAAAAAAAAAAAAHQAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATAB
+QAFQAWABcAGAAZABoAGwAcAB0AHgAfACAAIQAiACMAJAAlACYAJwAoACkAKgArACwALQAuAC8AMAA
+xADIAMwA0ADUANgA3ADgAOQA6ADsAPAA9AD4APwBAAEEAQgBDAEQARQBGAEcASABJAEoASwBMAE0A
+TgBPAFAAUQBSAFMAVABVAFYAVwBYAFkAWgBbAFwAXQBeAF8AYABhAIsAqQCDAJMAjQDDAKoAtgC3A
+LQAtQCrAL4AvwC8AIwAwADBAAAAAAAB//8AAgABAAAADAAAABwAAAACAAIAAwBxAAEAcgBzAAIABA
+AAAAIAAAABAAAACgBMAGYAAkRGTFQADmxhdG4AGgAEAAAAAP//AAEAAAAWAANDQVQgAB5NT0wgABZ
+ST00gABYAAP//AAEAAAAA//8AAgAAAAEAAmxpZ2EADmxvY2wAFAAAAAEAAQAAAAEAAAACAAYAEAAG
+AAAAAgASADQABAAAAAEATAADAAAAAgAQABYAAQAcAAAAAQABAE8AAQABAGcAAQABAE8AAwAAAAIAE
+AAWAAEAHAAAAAEAAQAvAAEAAQBnAAEAAQAvAAEAGgABAAgAAgAGAAwAcwACAE8AcgACAEwAAQABAE
+kAAAABAAAACgBGAGAAAkRGTFQADmxhdG4AHAAEAAAAAP//AAIAAAABABYAA0NBVCAAFk1PTCAAFlJ
+PTSAAFgAA//8AAgAAAAEAAmNwc3AADmtlcm4AFAAAAAEAAAAAAAEAAQACAAYADgABAAAAAQASAAIA
+AAACAB4ANgABAAoABQAFAAoAAgABACQAPQAAAAEAEgAEAAAAAQAMAAEAOP/nAAEAAQAkAAIGigAEA
+AAFJAXKABoAGQAA//gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAD/sv+4/+z/7v/MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAD/xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9T/6AAAAAD/8QAA
+ABD/vQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7gAAAAAAAAAAAAAAAAAA//MAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIAAAAAAAAAAP/5AAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAD/4AAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//L/9AAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAA/+gAAAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/zAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAD
+/4gAA//AAAAAA//YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/+AAAAAAAAP/OAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/zv/qAAAAAP/0AAAACAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAD/egAA/1kAAAAA/5D/rgAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAD/9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAD/8AAA/7b/8P+wAAD/8P/E/98AAAAA/8P/+P/0//oAAAAAAAAAAAAA//gA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+AAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/w//C/9MAAP/SAAD/9wAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAD/yAAA/+kAAAAA//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9wAAAAD//QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAP/cAAAAAAAAAAAAAAAA/7YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAP/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAkAFAAEAAAAAQACwAAABcA
+BgAAAAAAAAAIAA4AAAAAAAsAEgAAAAAAAAATABkAAwANAAAAAQAJAAAAAAAAAAAAAAAAAAAAGAAAA
+AAABwAAAAAAAAAAAAAAFQAFAAAAAAAYABgAAAAUAAAACgAAAAwAAgAPABEAFgAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAEAEQBdAAYAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAcAAAAAAAAABwAAAAAACAAAAAAAAAAAAAcAAAAHAAAAEwAJ
+ABUADgAPAAAACwAQAAAAAAAAAAAAAAAAAAUAGAACAAIAAgAAAAIAGAAXAAAAGAAAABYAFgACABYAA
+gAWAAAAEQADAAoAFAAMAA0ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASAAAAEgAGAAEAHgAkAC
+YAJwApACoALQAuAC8AMgAzADcAOAA5ADoAPAA9AEUASABOAE8AUgBTAFUAVwBZAFoAWwBcAF0AcwA
+AAAAAAQAAAADa3tfFAAAAANAan9kAAAAA4QodoQ==
+""")),
+ 10 if size is None else size,
+ layout_engine=Layout.BASIC,
+ )
+ return load_default_imagefont()
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageGrab.py b/venv/lib/python3.11/site-packages/PIL/ImageGrab.py
new file mode 100644
index 000000000..52cbfea40
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageGrab.py
@@ -0,0 +1,234 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# screen grabber
+#
+# History:
+# 2001-04-26 fl created
+# 2001-09-17 fl use builtin driver, if present
+# 2002-11-19 fl added grabclipboard support
+#
+# Copyright (c) 2001-2002 by Secret Labs AB
+# Copyright (c) 2001-2002 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from . import Image
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from . import ImageWin
+
+
+def grab(
+ bbox: tuple[int, int, int, int] | None = None,
+ include_layered_windows: bool = False,
+ all_screens: bool = False,
+ xdisplay: str | None = None,
+ window: int | ImageWin.HWND | None = None,
+ *,
+ scale_down: bool = False,
+) -> Image.Image:
+ im: Image.Image
+ if xdisplay is None:
+ if sys.platform == "darwin":
+ fh, filepath = tempfile.mkstemp(".png")
+ os.close(fh)
+ args = ["screencapture"]
+ if window is not None:
+ args += ["-l", str(window)]
+ elif bbox:
+ left, top, right, bottom = bbox
+ args += ["-R", f"{left},{top},{right-left},{bottom-top}"]
+ args += ["-x", filepath]
+ retcode = subprocess.call(args)
+ if retcode:
+ raise subprocess.CalledProcessError(retcode, args)
+ im = Image.open(filepath)
+ im.load()
+ os.unlink(filepath)
+ if bbox:
+ if window is not None:
+ # Determine if the window was in Retina mode or not
+ # by capturing it without the shadow,
+ # and checking how different the width is
+ fh, filepath = tempfile.mkstemp(".png")
+ os.close(fh)
+ args = ["screencapture", "-l", str(window), "-o", "-x", filepath]
+ retcode = subprocess.call(args)
+ if retcode:
+ raise subprocess.CalledProcessError(retcode, args)
+ with Image.open(filepath) as im_no_shadow:
+ retina = im.width - im_no_shadow.width > 100
+ os.unlink(filepath)
+
+ # Since screencapture's -R does not work with -l,
+ # crop the image manually
+ if retina:
+ left, top, right, bottom = bbox
+ scale = 1 if scale_down else 2
+ im_cropped = im.resize(
+ ((right - left) * scale, (bottom - top) * scale),
+ box=tuple(coord * 2 for coord in bbox),
+ )
+ else:
+ im_cropped = im.crop(bbox)
+ im.close()
+ return im_cropped
+ elif scale_down:
+ im_resized = im.resize((right - left, bottom - top))
+ im.close()
+ return im_resized
+ return im
+ elif sys.platform == "win32":
+ if window is not None:
+ all_screens = -1
+ offset, size, data = Image.core.grabscreen_win32(
+ include_layered_windows,
+ all_screens,
+ int(window) if window is not None else 0,
+ )
+ im = Image.frombytes(
+ "RGB",
+ size,
+ data,
+ # RGB, 32-bit line padding, origin lower left corner
+ "raw",
+ "BGR",
+ (size[0] * 3 + 3) & -4,
+ -1,
+ )
+ if bbox:
+ x0, y0 = offset
+ left, top, right, bottom = bbox
+ im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
+ return im
+ # Cast to Optional[str] needed for Windows and macOS.
+ display_name: str | None = xdisplay
+ try:
+ if not Image.core.HAVE_XCB:
+ msg = "Pillow was built without XCB support"
+ raise OSError(msg)
+ size, data = Image.core.grabscreen_x11(display_name)
+ except OSError:
+ if display_name is None and sys.platform not in ("darwin", "win32"):
+ if shutil.which("gnome-screenshot"):
+ args = ["gnome-screenshot", "-f"]
+ elif shutil.which("grim"):
+ args = ["grim"]
+ elif shutil.which("spectacle"):
+ args = ["spectacle", "-n", "-b", "-f", "-o"]
+ else:
+ raise
+ fh, filepath = tempfile.mkstemp(".png")
+ os.close(fh)
+ args.append(filepath)
+ retcode = subprocess.call(args)
+ if retcode:
+ raise subprocess.CalledProcessError(retcode, args)
+ im = Image.open(filepath)
+ im.load()
+ os.unlink(filepath)
+ if bbox:
+ im_cropped = im.crop(bbox)
+ im.close()
+ return im_cropped
+ return im
+ else:
+ raise
+ else:
+ im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
+ if bbox:
+ im = im.crop(bbox)
+ return im
+
+
+def grabclipboard() -> Image.Image | list[str] | None:
+ if sys.platform == "darwin":
+ p = subprocess.run(
+ ["osascript", "-e", "get the clipboard as «class PNGf»"],
+ capture_output=True,
+ )
+ if p.returncode != 0:
+ return None
+
+ import binascii
+
+ data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3]))
+ return Image.open(data)
+ elif sys.platform == "win32":
+ fmt, data = Image.core.grabclipboard_win32()
+ if fmt == "file": # CF_HDROP
+ import struct
+
+ o = struct.unpack_from("I", data)[0]
+ if data[16] == 0:
+ files = data[o:].decode("mbcs").split("\0")
+ else:
+ files = data[o:].decode("utf-16le").split("\0")
+ return files[: files.index("")]
+ if isinstance(data, bytes):
+ data = io.BytesIO(data)
+ if fmt == "png":
+ from . import PngImagePlugin
+
+ return PngImagePlugin.PngImageFile(data)
+ elif fmt == "DIB":
+ from . import BmpImagePlugin
+
+ return BmpImagePlugin.DibImageFile(data)
+ return None
+ else:
+ if os.getenv("WAYLAND_DISPLAY"):
+ session_type = "wayland"
+ elif os.getenv("DISPLAY"):
+ session_type = "x11"
+ else: # Session type check failed
+ session_type = None
+
+ if shutil.which("wl-paste") and session_type in ("wayland", None):
+ args = ["wl-paste", "-t", "image"]
+ elif shutil.which("xclip") and session_type in ("x11", None):
+ args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
+ else:
+ msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
+ raise NotImplementedError(msg)
+
+ p = subprocess.run(args, capture_output=True)
+ if p.returncode != 0:
+ err = p.stderr
+ for silent_error in [
+ # wl-paste, when the clipboard is empty
+ b"Nothing is copied",
+ # Ubuntu/Debian wl-paste, when the clipboard is empty
+ b"No selection",
+ # Ubuntu/Debian wl-paste, when an image isn't available
+ b"No suitable type of content copied",
+ # wl-paste or Ubuntu/Debian xclip, when an image isn't available
+ b" not available",
+ # xclip, when an image isn't available
+ b"cannot convert ",
+ # xclip, when the clipboard isn't initialized
+ b"xclip: Error: There is no owner for the ",
+ ]:
+ if silent_error in err:
+ return None
+ msg = f"{args[0]} error"
+ if err:
+ msg += f": {err.strip().decode()}"
+ raise ChildProcessError(msg)
+
+ data = io.BytesIO(p.stdout)
+ im = Image.open(data)
+ im.load()
+ return im
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageMath.py b/venv/lib/python3.11/site-packages/PIL/ImageMath.py
new file mode 100644
index 000000000..f62c6a3f8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageMath.py
@@ -0,0 +1,314 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# a simple math add-on for the Python Imaging Library
+#
+# History:
+# 1999-02-15 fl Original PIL Plus release
+# 2005-05-05 fl Simplified and cleaned up for PIL 1.1.6
+# 2005-09-12 fl Fixed int() and float() for Python 2.4.1
+#
+# Copyright (c) 1999-2005 by Secret Labs AB
+# Copyright (c) 2005 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import builtins
+
+from . import Image, _imagingmath
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from types import CodeType
+ from typing import Any
+
+
+class _Operand:
+ """Wraps an image operand, providing standard operators"""
+
+ def __init__(self, im: Image.Image):
+ self.im = im
+
+ def __fixup(self, im1: _Operand | float) -> Image.Image:
+ # convert image to suitable mode
+ if isinstance(im1, _Operand):
+ # argument was an image.
+ if im1.im.mode in ("1", "L"):
+ return im1.im.convert("I")
+ elif im1.im.mode in ("I", "F"):
+ return im1.im
+ else:
+ msg = f"unsupported mode: {im1.im.mode}"
+ raise ValueError(msg)
+ else:
+ # argument was a constant
+ if isinstance(im1, (int, float)) and self.im.mode in ("1", "L", "I"):
+ return Image.new("I", self.im.size, im1)
+ else:
+ return Image.new("F", self.im.size, im1)
+
+ def apply(
+ self,
+ op: str,
+ im1: _Operand | float,
+ im2: _Operand | float | None = None,
+ mode: str | None = None,
+ ) -> _Operand:
+ im_1 = self.__fixup(im1)
+ if im2 is None:
+ # unary operation
+ out = Image.new(mode or im_1.mode, im_1.size, None)
+ try:
+ op = getattr(_imagingmath, f"{op}_{im_1.mode}")
+ except AttributeError as e:
+ msg = f"bad operand type for '{op}'"
+ raise TypeError(msg) from e
+ _imagingmath.unop(op, out.getim(), im_1.getim())
+ else:
+ # binary operation
+ im_2 = self.__fixup(im2)
+ if im_1.mode != im_2.mode:
+ # convert both arguments to floating point
+ if im_1.mode != "F":
+ im_1 = im_1.convert("F")
+ if im_2.mode != "F":
+ im_2 = im_2.convert("F")
+ if im_1.size != im_2.size:
+ # crop both arguments to a common size
+ size = (
+ min(im_1.size[0], im_2.size[0]),
+ min(im_1.size[1], im_2.size[1]),
+ )
+ if im_1.size != size:
+ im_1 = im_1.crop((0, 0) + size)
+ if im_2.size != size:
+ im_2 = im_2.crop((0, 0) + size)
+ out = Image.new(mode or im_1.mode, im_1.size, None)
+ try:
+ op = getattr(_imagingmath, f"{op}_{im_1.mode}")
+ except AttributeError as e:
+ msg = f"bad operand type for '{op}'"
+ raise TypeError(msg) from e
+ _imagingmath.binop(op, out.getim(), im_1.getim(), im_2.getim())
+ return _Operand(out)
+
+ # unary operators
+ def __bool__(self) -> bool:
+ # an image is "true" if it contains at least one non-zero pixel
+ return self.im.getbbox() is not None
+
+ def __abs__(self) -> _Operand:
+ return self.apply("abs", self)
+
+ def __pos__(self) -> _Operand:
+ return self
+
+ def __neg__(self) -> _Operand:
+ return self.apply("neg", self)
+
+ # binary operators
+ def __add__(self, other: _Operand | float) -> _Operand:
+ return self.apply("add", self, other)
+
+ def __radd__(self, other: _Operand | float) -> _Operand:
+ return self.apply("add", other, self)
+
+ def __sub__(self, other: _Operand | float) -> _Operand:
+ return self.apply("sub", self, other)
+
+ def __rsub__(self, other: _Operand | float) -> _Operand:
+ return self.apply("sub", other, self)
+
+ def __mul__(self, other: _Operand | float) -> _Operand:
+ return self.apply("mul", self, other)
+
+ def __rmul__(self, other: _Operand | float) -> _Operand:
+ return self.apply("mul", other, self)
+
+ def __truediv__(self, other: _Operand | float) -> _Operand:
+ return self.apply("div", self, other)
+
+ def __rtruediv__(self, other: _Operand | float) -> _Operand:
+ return self.apply("div", other, self)
+
+ def __mod__(self, other: _Operand | float) -> _Operand:
+ return self.apply("mod", self, other)
+
+ def __rmod__(self, other: _Operand | float) -> _Operand:
+ return self.apply("mod", other, self)
+
+ def __pow__(self, other: _Operand | float) -> _Operand:
+ return self.apply("pow", self, other)
+
+ def __rpow__(self, other: _Operand | float) -> _Operand:
+ return self.apply("pow", other, self)
+
+ # bitwise
+ def __invert__(self) -> _Operand:
+ return self.apply("invert", self)
+
+ def __and__(self, other: _Operand | float) -> _Operand:
+ return self.apply("and", self, other)
+
+ def __rand__(self, other: _Operand | float) -> _Operand:
+ return self.apply("and", other, self)
+
+ def __or__(self, other: _Operand | float) -> _Operand:
+ return self.apply("or", self, other)
+
+ def __ror__(self, other: _Operand | float) -> _Operand:
+ return self.apply("or", other, self)
+
+ def __xor__(self, other: _Operand | float) -> _Operand:
+ return self.apply("xor", self, other)
+
+ def __rxor__(self, other: _Operand | float) -> _Operand:
+ return self.apply("xor", other, self)
+
+ def __lshift__(self, other: _Operand | float) -> _Operand:
+ return self.apply("lshift", self, other)
+
+ def __rshift__(self, other: _Operand | float) -> _Operand:
+ return self.apply("rshift", self, other)
+
+ # logical
+ def __eq__(self, other: _Operand | float) -> _Operand: # type: ignore[override]
+ return self.apply("eq", self, other)
+
+ def __ne__(self, other: _Operand | float) -> _Operand: # type: ignore[override]
+ return self.apply("ne", self, other)
+
+ def __lt__(self, other: _Operand | float) -> _Operand:
+ return self.apply("lt", self, other)
+
+ def __le__(self, other: _Operand | float) -> _Operand:
+ return self.apply("le", self, other)
+
+ def __gt__(self, other: _Operand | float) -> _Operand:
+ return self.apply("gt", self, other)
+
+ def __ge__(self, other: _Operand | float) -> _Operand:
+ return self.apply("ge", self, other)
+
+
+# conversions
+def imagemath_int(self: _Operand) -> _Operand:
+ return _Operand(self.im.convert("I"))
+
+
+def imagemath_float(self: _Operand) -> _Operand:
+ return _Operand(self.im.convert("F"))
+
+
+# logical
+def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand:
+ return self.apply("eq", self, other, mode="I")
+
+
+def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand:
+ return self.apply("ne", self, other, mode="I")
+
+
+def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand:
+ return self.apply("min", self, other)
+
+
+def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand:
+ return self.apply("max", self, other)
+
+
+def imagemath_convert(self: _Operand, mode: str) -> _Operand:
+ return _Operand(self.im.convert(mode))
+
+
+ops = {
+ "int": imagemath_int,
+ "float": imagemath_float,
+ "equal": imagemath_equal,
+ "notequal": imagemath_notequal,
+ "min": imagemath_min,
+ "max": imagemath_max,
+ "convert": imagemath_convert,
+}
+
+
+def lambda_eval(expression: Callable[[dict[str, Any]], Any], **kw: Any) -> Any:
+ """
+ Returns the result of an image function.
+
+ :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band
+ images, use the :py:meth:`~PIL.Image.Image.split` method or
+ :py:func:`~PIL.Image.merge` function.
+
+ :param expression: A function that receives a dictionary.
+ :param **kw: Values to add to the function's dictionary.
+ :return: The expression result. This is usually an image object, but can
+ also be an integer, a floating point value, or a pixel tuple,
+ depending on the expression.
+ """
+
+ args: dict[str, Any] = ops.copy()
+ args.update(kw)
+ for k, v in args.items():
+ if isinstance(v, Image.Image):
+ args[k] = _Operand(v)
+
+ out = expression(args)
+ try:
+ return out.im
+ except AttributeError:
+ return out
+
+
+def unsafe_eval(expression: str, **kw: Any) -> Any:
+ """
+ Evaluates an image expression. This uses Python's ``eval()`` function to process
+ the expression string, and carries the security risks of doing so. It is not
+ recommended to process expressions without considering this.
+ :py:meth:`~lambda_eval` is a more secure alternative.
+
+ :py:mod:`~PIL.ImageMath` only supports single-layer images. To process multi-band
+ images, use the :py:meth:`~PIL.Image.Image.split` method or
+ :py:func:`~PIL.Image.merge` function.
+
+ :param expression: A string containing a Python-style expression.
+ :param **kw: Values to add to the evaluation context.
+ :return: The evaluated expression. This is usually an image object, but can
+ also be an integer, a floating point value, or a pixel tuple,
+ depending on the expression.
+ """
+
+ # build execution namespace
+ args: dict[str, Any] = ops.copy()
+ for k in kw:
+ if "__" in k or hasattr(builtins, k):
+ msg = f"'{k}' not allowed"
+ raise ValueError(msg)
+
+ args.update(kw)
+ for k, v in args.items():
+ if isinstance(v, Image.Image):
+ args[k] = _Operand(v)
+
+ compiled_code = compile(expression, "", "eval")
+
+ def scan(code: CodeType) -> None:
+ for const in code.co_consts:
+ if type(const) is type(compiled_code):
+ scan(const)
+
+ for name in code.co_names:
+ if name not in args and name != "abs":
+ msg = f"'{name}' not allowed"
+ raise ValueError(msg)
+
+ scan(compiled_code)
+ out = builtins.eval(expression, None, args)
+ try:
+ return out.im
+ except AttributeError:
+ return out
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageMode.py b/venv/lib/python3.11/site-packages/PIL/ImageMode.py
new file mode 100644
index 000000000..b7c6c8636
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageMode.py
@@ -0,0 +1,85 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# standard mode descriptors
+#
+# History:
+# 2006-03-20 fl Added
+#
+# Copyright (c) 2006 by Secret Labs AB.
+# Copyright (c) 2006 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import sys
+from functools import lru_cache
+from typing import NamedTuple
+
+
+class ModeDescriptor(NamedTuple):
+ """Wrapper for mode strings."""
+
+ mode: str
+ bands: tuple[str, ...]
+ basemode: str
+ basetype: str
+ typestr: str
+
+ def __str__(self) -> str:
+ return self.mode
+
+
+@lru_cache
+def getmode(mode: str) -> ModeDescriptor:
+ """Gets a mode descriptor for the given mode."""
+ endian = "<" if sys.byteorder == "little" else ">"
+
+ modes = {
+ # core modes
+ # Bits need to be extended to bytes
+ "1": ("L", "L", ("1",), "|b1"),
+ "L": ("L", "L", ("L",), "|u1"),
+ "I": ("L", "I", ("I",), f"{endian}i4"),
+ "F": ("L", "F", ("F",), f"{endian}f4"),
+ "P": ("P", "L", ("P",), "|u1"),
+ "RGB": ("RGB", "L", ("R", "G", "B"), "|u1"),
+ "RGBX": ("RGB", "L", ("R", "G", "B", "X"), "|u1"),
+ "RGBA": ("RGB", "L", ("R", "G", "B", "A"), "|u1"),
+ "CMYK": ("RGB", "L", ("C", "M", "Y", "K"), "|u1"),
+ "YCbCr": ("RGB", "L", ("Y", "Cb", "Cr"), "|u1"),
+ # UNDONE - unsigned |u1i1i1
+ "LAB": ("RGB", "L", ("L", "A", "B"), "|u1"),
+ "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"),
+ # extra experimental modes
+ "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"),
+ "LA": ("L", "L", ("L", "A"), "|u1"),
+ "La": ("L", "L", ("L", "a"), "|u1"),
+ "PA": ("RGB", "L", ("P", "A"), "|u1"),
+ }
+ if mode in modes:
+ base_mode, base_type, bands, type_str = modes[mode]
+ return ModeDescriptor(mode, bands, base_mode, base_type, type_str)
+
+ mapping_modes = {
+ # I;16 == I;16L, and I;32 == I;32L
+ "I;16": "u2",
+ "I;16BS": ">i2",
+ "I;16N": f"{endian}u2",
+ "I;16NS": f"{endian}i2",
+ "I;32": "u4",
+ "I;32L": "i4",
+ "I;32LS": "
+from __future__ import annotations
+
+import re
+
+from . import Image, _imagingmorph
+
+LUT_SIZE = 1 << 9
+
+# fmt: off
+ROTATION_MATRIX = [
+ 6, 3, 0,
+ 7, 4, 1,
+ 8, 5, 2,
+]
+MIRROR_MATRIX = [
+ 2, 1, 0,
+ 5, 4, 3,
+ 8, 7, 6,
+]
+# fmt: on
+
+
+class LutBuilder:
+ """A class for building a MorphLut from a descriptive language
+
+ The input patterns is a list of a strings sequences like these::
+
+ 4:(...
+ .1.
+ 111)->1
+
+ (whitespaces including linebreaks are ignored). The option 4
+ describes a series of symmetry operations (in this case a
+ 4-rotation), the pattern is described by:
+
+ - . or X - Ignore
+ - 1 - Pixel is on
+ - 0 - Pixel is off
+
+ The result of the operation is described after "->" string.
+
+ The default is to return the current pixel value, which is
+ returned if no other match is found.
+
+ Operations:
+
+ - 4 - 4 way rotation
+ - N - Negate
+ - 1 - Dummy op for no other operation (an op must always be given)
+ - M - Mirroring
+
+ Example::
+
+ lb = LutBuilder(patterns = ["4:(... .1. 111)->1"])
+ lut = lb.build_lut()
+
+ """
+
+ def __init__(
+ self, patterns: list[str] | None = None, op_name: str | None = None
+ ) -> None:
+ """
+ :param patterns: A list of input patterns, or None.
+ :param op_name: The name of a known pattern. One of "corner", "dilation4",
+ "dilation8", "erosion4", "erosion8" or "edge".
+ :exception Exception: If the op_name is not recognized.
+ """
+ self.lut: bytearray | None = None
+ if op_name is not None:
+ known_patterns = {
+ "corner": ["1:(... ... ...)->0", "4:(00. 01. ...)->1"],
+ "dilation4": ["4:(... .0. .1.)->1"],
+ "dilation8": ["4:(... .0. .1.)->1", "4:(... .0. ..1)->1"],
+ "erosion4": ["4:(... .1. .0.)->0"],
+ "erosion8": ["4:(... .1. .0.)->0", "4:(... .1. ..0)->0"],
+ "edge": [
+ "1:(... ... ...)->0",
+ "4:(.0. .1. ...)->1",
+ "4:(01. .1. ...)->1",
+ ],
+ }
+ if op_name not in known_patterns:
+ msg = f"Unknown pattern {op_name}!"
+ raise Exception(msg)
+
+ self.patterns = known_patterns[op_name]
+ elif patterns is not None:
+ self.patterns = patterns
+ else:
+ self.patterns = []
+
+ def add_patterns(self, patterns: list[str]) -> None:
+ """
+ Append to list of patterns.
+
+ :param patterns: Additional patterns.
+ """
+ self.patterns += patterns
+
+ def build_default_lut(self) -> bytearray:
+ """
+ Set the current LUT, and return it.
+
+ This is the default LUT that patterns will be applied against when building.
+ """
+ symbols = [0, 1]
+ m = 1 << 4 # pos of current pixel
+ self.lut = bytearray(symbols[(i & m) > 0] for i in range(LUT_SIZE))
+ return self.lut
+
+ def get_lut(self) -> bytearray | None:
+ """
+ Returns the current LUT
+ """
+ return self.lut
+
+ def _string_permute(self, pattern: str, permutation: list[int]) -> str:
+ """Takes a pattern and a permutation and returns the
+ string permuted according to the permutation list.
+ """
+ assert len(permutation) == 9
+ return "".join(pattern[p] for p in permutation)
+
+ def _pattern_permute(
+ self, basic_pattern: str, options: str, basic_result: int
+ ) -> list[tuple[str, int]]:
+ """Takes a basic pattern and its result and clones
+ the pattern according to the modifications described in the $options
+ parameter. It returns a list of all cloned patterns."""
+ patterns = [(basic_pattern, basic_result)]
+
+ # rotations
+ if "4" in options:
+ res = patterns[-1][1]
+ for i in range(4):
+ patterns.append(
+ (self._string_permute(patterns[-1][0], ROTATION_MATRIX), res)
+ )
+ # mirror
+ if "M" in options:
+ n = len(patterns)
+ for pattern, res in patterns[:n]:
+ patterns.append((self._string_permute(pattern, MIRROR_MATRIX), res))
+
+ # negate
+ if "N" in options:
+ n = len(patterns)
+ for pattern, res in patterns[:n]:
+ # Swap 0 and 1
+ pattern = pattern.replace("0", "Z").replace("1", "0").replace("Z", "1")
+ res = 1 - int(res)
+ patterns.append((pattern, res))
+
+ return patterns
+
+ def build_lut(self) -> bytearray:
+ """Compile all patterns into a morphology LUT, and return it.
+
+ This is the data to be passed into MorphOp."""
+ self.build_default_lut()
+ assert self.lut is not None
+ patterns = []
+
+ # Parse and create symmetries of the patterns strings
+ for p in self.patterns:
+ m = re.search(r"(\w):?\s*\((.+?)\)\s*->\s*(\d)", p.replace("\n", ""))
+ if not m:
+ msg = 'Syntax error in pattern "' + p + '"'
+ raise Exception(msg)
+ options = m.group(1)
+ pattern = m.group(2)
+ result = int(m.group(3))
+
+ # Get rid of spaces
+ pattern = pattern.replace(" ", "").replace("\n", "")
+
+ patterns += self._pattern_permute(pattern, options, result)
+
+ # Compile the patterns into regular expressions for speed
+ compiled_patterns = []
+ for pattern in patterns:
+ p = pattern[0].replace(".", "X").replace("X", "[01]")
+ compiled_patterns.append((re.compile(p), pattern[1]))
+
+ # Step through table and find patterns that match.
+ # Note that all the patterns are searched. The last one found takes priority
+ for i in range(LUT_SIZE):
+ # Build the bit pattern
+ bitpattern = bin(i)[2:]
+ bitpattern = ("0" * (9 - len(bitpattern)) + bitpattern)[::-1]
+
+ for pattern, r in compiled_patterns:
+ if pattern.match(bitpattern):
+ self.lut[i] = [0, 1][r]
+
+ return self.lut
+
+
+class MorphOp:
+ """A class for binary morphological operators"""
+
+ def __init__(
+ self,
+ lut: bytearray | None = None,
+ op_name: str | None = None,
+ patterns: list[str] | None = None,
+ ) -> None:
+ """Create a binary morphological operator.
+
+ If the LUT is not provided, then it is built using LutBuilder from the op_name
+ or the patterns.
+
+ :param lut: The LUT data.
+ :param patterns: A list of input patterns, or None.
+ :param op_name: The name of a known pattern. One of "corner", "dilation4",
+ "dilation8", "erosion4", "erosion8", "edge".
+ :exception Exception: If the op_name is not recognized.
+ """
+ if patterns is None and op_name is None:
+ self.lut = lut
+ else:
+ self.lut = LutBuilder(patterns, op_name).build_lut()
+
+ def apply(self, image: Image.Image) -> tuple[int, Image.Image]:
+ """Run a single morphological operation on an image.
+
+ Returns a tuple of the number of changed pixels and the
+ morphed image.
+
+ :param image: A 1-mode or L-mode image.
+ :exception Exception: If the current operator is None.
+ :exception ValueError: If the image is not 1 or L mode."""
+ if self.lut is None:
+ msg = "No operator loaded"
+ raise Exception(msg)
+
+ if image.mode not in ("1", "L"):
+ msg = "Image mode must be 1 or L"
+ raise ValueError(msg)
+ outimage = Image.new(image.mode, image.size)
+ count = _imagingmorph.apply(bytes(self.lut), image.getim(), outimage.getim())
+ return count, outimage
+
+ def match(self, image: Image.Image) -> list[tuple[int, int]]:
+ """Get a list of coordinates matching the morphological operation on
+ an image.
+
+ Returns a list of tuples of (x,y) coordinates of all matching pixels. See
+ :ref:`coordinate-system`.
+
+ :param image: A 1-mode or L-mode image.
+ :exception Exception: If the current operator is None.
+ :exception ValueError: If the image is not 1 or L mode."""
+ if self.lut is None:
+ msg = "No operator loaded"
+ raise Exception(msg)
+
+ if image.mode not in ("1", "L"):
+ msg = "Image mode must be 1 or L"
+ raise ValueError(msg)
+ return _imagingmorph.match(bytes(self.lut), image.getim())
+
+ def get_on_pixels(self, image: Image.Image) -> list[tuple[int, int]]:
+ """Get a list of all turned on pixels in a 1 or L mode image.
+
+ Returns a list of tuples of (x,y) coordinates of all non-empty pixels. See
+ :ref:`coordinate-system`.
+
+ :param image: A 1-mode or L-mode image.
+ :exception ValueError: If the image is not 1 or L mode."""
+
+ if image.mode not in ("1", "L"):
+ msg = "Image mode must be 1 or L"
+ raise ValueError(msg)
+ return _imagingmorph.get_on_pixels(image.getim())
+
+ def load_lut(self, filename: str) -> None:
+ """
+ Load an operator from an mrl file
+
+ :param filename: The file to read from.
+ :exception Exception: If the length of the file data is not 512.
+ """
+ with open(filename, "rb") as f:
+ self.lut = bytearray(f.read())
+
+ if len(self.lut) != LUT_SIZE:
+ self.lut = None
+ msg = "Wrong size operator file!"
+ raise Exception(msg)
+
+ def save_lut(self, filename: str) -> None:
+ """
+ Save an operator to an mrl file.
+
+ :param filename: The destination file.
+ :exception Exception: If the current operator is None.
+ """
+ if self.lut is None:
+ msg = "No operator loaded"
+ raise Exception(msg)
+ with open(filename, "wb") as f:
+ f.write(self.lut)
+
+ def set_lut(self, lut: bytearray | None) -> None:
+ """
+ Set the LUT from an external source
+
+ :param lut: A new LUT.
+ """
+ self.lut = lut
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageOps.py b/venv/lib/python3.11/site-packages/PIL/ImageOps.py
new file mode 100644
index 000000000..f0ae142b9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageOps.py
@@ -0,0 +1,749 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# standard image operations
+#
+# History:
+# 2001-10-20 fl Created
+# 2001-10-23 fl Added autocontrast operator
+# 2001-12-18 fl Added Kevin's fit operator
+# 2004-03-14 fl Fixed potential division by zero in equalize
+# 2005-05-05 fl Fixed equalize for low number of values
+#
+# Copyright (c) 2001-2004 by Secret Labs AB
+# Copyright (c) 2001-2004 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import functools
+import operator
+import re
+from collections.abc import Sequence
+from typing import Literal, Protocol, cast, overload
+
+from . import ExifTags, Image, ImagePalette
+
+#
+# helpers
+
+
+def _border(border: int | tuple[int, ...]) -> tuple[int, int, int, int]:
+ if isinstance(border, tuple):
+ if len(border) == 2:
+ left, top = right, bottom = border
+ elif len(border) == 4:
+ left, top, right, bottom = border
+ else:
+ msg = "border must be an integer, or a tuple of two or four elements"
+ raise ValueError(msg)
+ else:
+ left = top = right = bottom = border
+ return left, top, right, bottom
+
+
+def _color(color: str | int | tuple[int, ...], mode: str) -> int | tuple[int, ...]:
+ if isinstance(color, str):
+ from . import ImageColor
+
+ color = ImageColor.getcolor(color, mode)
+ return color
+
+
+def _lut(image: Image.Image, lut: list[int]) -> Image.Image:
+ if image.mode == "P":
+ # FIXME: apply to lookup table, not image data
+ msg = "mode P support coming soon"
+ raise NotImplementedError(msg)
+ elif image.mode in ("L", "RGB"):
+ if image.mode == "RGB" and len(lut) == 256:
+ lut = lut + lut + lut
+ return image.point(lut)
+ else:
+ msg = f"not supported for mode {image.mode}"
+ raise OSError(msg)
+
+
+#
+# actions
+
+
+def autocontrast(
+ image: Image.Image,
+ cutoff: float | tuple[float, float] = 0,
+ ignore: int | Sequence[int] | None = None,
+ mask: Image.Image | None = None,
+ preserve_tone: bool = False,
+) -> Image.Image:
+ """
+ Maximize (normalize) image contrast. This function calculates a
+ histogram of the input image (or mask region), removes ``cutoff`` percent of the
+ lightest and darkest pixels from the histogram, and remaps the image
+ so that the darkest pixel becomes black (0), and the lightest
+ becomes white (255).
+
+ :param image: The image to process.
+ :param cutoff: The percent to cut off from the histogram on the low and
+ high ends. Either a tuple of (low, high), or a single
+ number for both.
+ :param ignore: The background pixel value (use None for no background).
+ :param mask: Histogram used in contrast operation is computed using pixels
+ within the mask. If no mask is given the entire image is used
+ for histogram computation.
+ :param preserve_tone: Preserve image tone in Photoshop-like style autocontrast.
+
+ .. versionadded:: 8.2.0
+
+ :return: An image.
+ """
+ if preserve_tone:
+ histogram = image.convert("L").histogram(mask)
+ else:
+ histogram = image.histogram(mask)
+
+ lut = []
+ for layer in range(0, len(histogram), 256):
+ h = histogram[layer : layer + 256]
+ if ignore is not None:
+ # get rid of outliers
+ if isinstance(ignore, int):
+ h[ignore] = 0
+ else:
+ for ix in ignore:
+ h[ix] = 0
+ if cutoff:
+ # cut off pixels from both ends of the histogram
+ if not isinstance(cutoff, tuple):
+ cutoff = (cutoff, cutoff)
+ # get number of pixels
+ n = 0
+ for ix in range(256):
+ n = n + h[ix]
+ # remove cutoff% pixels from the low end
+ cut = int(n * cutoff[0] // 100)
+ for lo in range(256):
+ if cut > h[lo]:
+ cut = cut - h[lo]
+ h[lo] = 0
+ else:
+ h[lo] -= cut
+ cut = 0
+ if cut <= 0:
+ break
+ # remove cutoff% samples from the high end
+ cut = int(n * cutoff[1] // 100)
+ for hi in range(255, -1, -1):
+ if cut > h[hi]:
+ cut = cut - h[hi]
+ h[hi] = 0
+ else:
+ h[hi] -= cut
+ cut = 0
+ if cut <= 0:
+ break
+ # find lowest/highest samples after preprocessing
+ for lo in range(256):
+ if h[lo]:
+ break
+ for hi in range(255, -1, -1):
+ if h[hi]:
+ break
+ if hi <= lo:
+ # don't bother
+ lut.extend(list(range(256)))
+ else:
+ scale = 255.0 / (hi - lo)
+ offset = -lo * scale
+ for ix in range(256):
+ ix = int(ix * scale + offset)
+ if ix < 0:
+ ix = 0
+ elif ix > 255:
+ ix = 255
+ lut.append(ix)
+ return _lut(image, lut)
+
+
+def colorize(
+ image: Image.Image,
+ black: str | tuple[int, ...],
+ white: str | tuple[int, ...],
+ mid: str | int | tuple[int, ...] | None = None,
+ blackpoint: int = 0,
+ whitepoint: int = 255,
+ midpoint: int = 127,
+) -> Image.Image:
+ """
+ Colorize grayscale image.
+ This function calculates a color wedge which maps all black pixels in
+ the source image to the first color and all white pixels to the
+ second color. If ``mid`` is specified, it uses three-color mapping.
+ The ``black`` and ``white`` arguments should be RGB tuples or color names;
+ optionally you can use three-color mapping by also specifying ``mid``.
+ Mapping positions for any of the colors can be specified
+ (e.g. ``blackpoint``), where these parameters are the integer
+ value corresponding to where the corresponding color should be mapped.
+ These parameters must have logical order, such that
+ ``blackpoint <= midpoint <= whitepoint`` (if ``mid`` is specified).
+
+ :param image: The image to colorize.
+ :param black: The color to use for black input pixels.
+ :param white: The color to use for white input pixels.
+ :param mid: The color to use for midtone input pixels.
+ :param blackpoint: an int value [0, 255] for the black mapping.
+ :param whitepoint: an int value [0, 255] for the white mapping.
+ :param midpoint: an int value [0, 255] for the midtone mapping.
+ :return: An image.
+ """
+
+ # Initial asserts
+ assert image.mode == "L"
+ if mid is None:
+ assert 0 <= blackpoint <= whitepoint <= 255
+ else:
+ assert 0 <= blackpoint <= midpoint <= whitepoint <= 255
+
+ # Define colors from arguments
+ rgb_black = cast(Sequence[int], _color(black, "RGB"))
+ rgb_white = cast(Sequence[int], _color(white, "RGB"))
+ rgb_mid = cast(Sequence[int], _color(mid, "RGB")) if mid is not None else None
+
+ # Empty lists for the mapping
+ red = []
+ green = []
+ blue = []
+
+ # Create the low-end values
+ for i in range(blackpoint):
+ red.append(rgb_black[0])
+ green.append(rgb_black[1])
+ blue.append(rgb_black[2])
+
+ # Create the mapping (2-color)
+ if rgb_mid is None:
+ range_map = range(whitepoint - blackpoint)
+
+ for i in range_map:
+ red.append(
+ rgb_black[0] + i * (rgb_white[0] - rgb_black[0]) // len(range_map)
+ )
+ green.append(
+ rgb_black[1] + i * (rgb_white[1] - rgb_black[1]) // len(range_map)
+ )
+ blue.append(
+ rgb_black[2] + i * (rgb_white[2] - rgb_black[2]) // len(range_map)
+ )
+
+ # Create the mapping (3-color)
+ else:
+ range_map1 = range(midpoint - blackpoint)
+ range_map2 = range(whitepoint - midpoint)
+
+ for i in range_map1:
+ red.append(
+ rgb_black[0] + i * (rgb_mid[0] - rgb_black[0]) // len(range_map1)
+ )
+ green.append(
+ rgb_black[1] + i * (rgb_mid[1] - rgb_black[1]) // len(range_map1)
+ )
+ blue.append(
+ rgb_black[2] + i * (rgb_mid[2] - rgb_black[2]) // len(range_map1)
+ )
+ for i in range_map2:
+ red.append(rgb_mid[0] + i * (rgb_white[0] - rgb_mid[0]) // len(range_map2))
+ green.append(
+ rgb_mid[1] + i * (rgb_white[1] - rgb_mid[1]) // len(range_map2)
+ )
+ blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2))
+
+ # Create the high-end values
+ for i in range(256 - whitepoint):
+ red.append(rgb_white[0])
+ green.append(rgb_white[1])
+ blue.append(rgb_white[2])
+
+ # Return converted image
+ image = image.convert("RGB")
+ return _lut(image, red + green + blue)
+
+
+def contain(
+ image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
+) -> Image.Image:
+ """
+ Returns a resized version of the image, set to the maximum width and height
+ within the requested size, while maintaining the original aspect ratio.
+
+ :param image: The image to resize.
+ :param size: The requested output size in pixels, given as a
+ (width, height) tuple.
+ :param method: Resampling method to use. Default is
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
+ :return: An image.
+ """
+
+ im_ratio = image.width / image.height
+ dest_ratio = size[0] / size[1]
+
+ if im_ratio != dest_ratio:
+ if im_ratio > dest_ratio:
+ new_height = round(image.height / image.width * size[0])
+ if new_height != size[1]:
+ size = (size[0], new_height)
+ else:
+ new_width = round(image.width / image.height * size[1])
+ if new_width != size[0]:
+ size = (new_width, size[1])
+ return image.resize(size, resample=method)
+
+
+def cover(
+ image: Image.Image, size: tuple[int, int], method: int = Image.Resampling.BICUBIC
+) -> Image.Image:
+ """
+ Returns a resized version of the image, so that the requested size is
+ covered, while maintaining the original aspect ratio.
+
+ :param image: The image to resize.
+ :param size: The requested output size in pixels, given as a
+ (width, height) tuple.
+ :param method: Resampling method to use. Default is
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
+ :return: An image.
+ """
+
+ im_ratio = image.width / image.height
+ dest_ratio = size[0] / size[1]
+
+ if im_ratio != dest_ratio:
+ if im_ratio < dest_ratio:
+ new_height = round(image.height / image.width * size[0])
+ if new_height != size[1]:
+ size = (size[0], new_height)
+ else:
+ new_width = round(image.width / image.height * size[1])
+ if new_width != size[0]:
+ size = (new_width, size[1])
+ return image.resize(size, resample=method)
+
+
+def pad(
+ image: Image.Image,
+ size: tuple[int, int],
+ method: int = Image.Resampling.BICUBIC,
+ color: str | int | tuple[int, ...] | None = None,
+ centering: tuple[float, float] = (0.5, 0.5),
+) -> Image.Image:
+ """
+ Returns a resized and padded version of the image, expanded to fill the
+ requested aspect ratio and size.
+
+ :param image: The image to resize and crop.
+ :param size: The requested output size in pixels, given as a
+ (width, height) tuple.
+ :param method: Resampling method to use. Default is
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
+ :param color: The background color of the padded image.
+ :param centering: Control the position of the original image within the
+ padded version.
+
+ (0.5, 0.5) will keep the image centered
+ (0, 0) will keep the image aligned to the top left
+ (1, 1) will keep the image aligned to the bottom
+ right
+ :return: An image.
+ """
+
+ resized = contain(image, size, method)
+ if resized.size == size:
+ out = resized
+ else:
+ out = Image.new(image.mode, size, color)
+ if resized.palette:
+ palette = resized.getpalette()
+ if palette is not None:
+ out.putpalette(palette)
+ if resized.width != size[0]:
+ x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
+ out.paste(resized, (x, 0))
+ else:
+ y = round((size[1] - resized.height) * max(0, min(centering[1], 1)))
+ out.paste(resized, (0, y))
+ return out
+
+
+def crop(image: Image.Image, border: int = 0) -> Image.Image:
+ """
+ Remove border from image. The same amount of pixels are removed
+ from all four sides. This function works on all image modes.
+
+ .. seealso:: :py:meth:`~PIL.Image.Image.crop`
+
+ :param image: The image to crop.
+ :param border: The number of pixels to remove.
+ :return: An image.
+ """
+ left, top, right, bottom = _border(border)
+ return image.crop((left, top, image.size[0] - right, image.size[1] - bottom))
+
+
+def scale(
+ image: Image.Image, factor: float, resample: int = Image.Resampling.BICUBIC
+) -> Image.Image:
+ """
+ Returns a rescaled image by a specific factor given in parameter.
+ A factor greater than 1 expands the image, between 0 and 1 contracts the
+ image.
+
+ :param image: The image to rescale.
+ :param factor: The expansion factor, as a float.
+ :param resample: Resampling method to use. Default is
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
+ :returns: An :py:class:`~PIL.Image.Image` object.
+ """
+ if factor == 1:
+ return image.copy()
+ elif factor <= 0:
+ msg = "the factor must be greater than 0"
+ raise ValueError(msg)
+ else:
+ size = (round(factor * image.width), round(factor * image.height))
+ return image.resize(size, resample)
+
+
+class SupportsGetMesh(Protocol):
+ """
+ An object that supports the ``getmesh`` method, taking an image as an
+ argument, and returning a list of tuples. Each tuple contains two tuples,
+ the source box as a tuple of 4 integers, and a tuple of 8 integers for the
+ final quadrilateral, in order of top left, bottom left, bottom right, top
+ right.
+ """
+
+ def getmesh(
+ self, image: Image.Image
+ ) -> list[
+ tuple[tuple[int, int, int, int], tuple[int, int, int, int, int, int, int, int]]
+ ]: ...
+
+
+def deform(
+ image: Image.Image,
+ deformer: SupportsGetMesh,
+ resample: int = Image.Resampling.BILINEAR,
+) -> Image.Image:
+ """
+ Deform the image.
+
+ :param image: The image to deform.
+ :param deformer: A deformer object. Any object that implements a
+ ``getmesh`` method can be used.
+ :param resample: An optional resampling filter. Same values possible as
+ in the PIL.Image.transform function.
+ :return: An image.
+ """
+ return image.transform(
+ image.size, Image.Transform.MESH, deformer.getmesh(image), resample
+ )
+
+
+def equalize(image: Image.Image, mask: Image.Image | None = None) -> Image.Image:
+ """
+ Equalize the image histogram. This function applies a non-linear
+ mapping to the input image, in order to create a uniform
+ distribution of grayscale values in the output image.
+
+ :param image: The image to equalize.
+ :param mask: An optional mask. If given, only the pixels selected by
+ the mask are included in the analysis.
+ :return: An image.
+ """
+ if image.mode == "P":
+ image = image.convert("RGB")
+ h = image.histogram(mask)
+ lut = []
+ for b in range(0, len(h), 256):
+ histo = [_f for _f in h[b : b + 256] if _f]
+ if len(histo) <= 1:
+ lut.extend(list(range(256)))
+ else:
+ step = (functools.reduce(operator.add, histo) - histo[-1]) // 255
+ if not step:
+ lut.extend(list(range(256)))
+ else:
+ n = step // 2
+ for i in range(256):
+ lut.append(n // step)
+ n = n + h[i + b]
+ return _lut(image, lut)
+
+
+def expand(
+ image: Image.Image,
+ border: int | tuple[int, ...] = 0,
+ fill: str | int | tuple[int, ...] = 0,
+) -> Image.Image:
+ """
+ Add border to the image
+
+ :param image: The image to expand.
+ :param border: Border width, in pixels.
+ :param fill: Pixel fill value (a color value). Default is 0 (black).
+ :return: An image.
+ """
+ left, top, right, bottom = _border(border)
+ width = left + image.size[0] + right
+ height = top + image.size[1] + bottom
+ color = _color(fill, image.mode)
+ if image.palette:
+ mode = image.palette.mode
+ palette = ImagePalette.ImagePalette(mode, image.getpalette(mode))
+ if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
+ color = palette.getcolor(color)
+ else:
+ palette = None
+ out = Image.new(image.mode, (width, height), color)
+ if palette:
+ out.putpalette(palette.palette, mode)
+ out.paste(image, (left, top))
+ return out
+
+
+def fit(
+ image: Image.Image,
+ size: tuple[int, int],
+ method: int = Image.Resampling.BICUBIC,
+ bleed: float = 0.0,
+ centering: tuple[float, float] = (0.5, 0.5),
+) -> Image.Image:
+ """
+ Returns a resized and cropped version of the image, cropped to the
+ requested aspect ratio and size.
+
+ This function was contributed by Kevin Cazabon.
+
+ :param image: The image to resize and crop.
+ :param size: The requested output size in pixels, given as a
+ (width, height) tuple.
+ :param method: Resampling method to use. Default is
+ :py:attr:`~PIL.Image.Resampling.BICUBIC`.
+ See :ref:`concept-filters`.
+ :param bleed: Remove a border around the outside of the image from all
+ four edges. The value is a decimal percentage (use 0.01 for
+ one percent). The default value is 0 (no border).
+ Cannot be greater than or equal to 0.5.
+ :param centering: Control the cropping position. Use (0.5, 0.5) for
+ center cropping (e.g. if cropping the width, take 50% off
+ of the left side, and therefore 50% off the right side).
+ (0.0, 0.0) will crop from the top left corner (i.e. if
+ cropping the width, take all of the crop off of the right
+ side, and if cropping the height, take all of it off the
+ bottom). (1.0, 0.0) will crop from the bottom left
+ corner, etc. (i.e. if cropping the width, take all of the
+ crop off the left side, and if cropping the height take
+ none from the top, and therefore all off the bottom).
+ :return: An image.
+ """
+
+ # by Kevin Cazabon, Feb 17/2000
+ # kevin@cazabon.com
+ # https://www.cazabon.com
+
+ centering_x, centering_y = centering
+
+ if not 0.0 <= centering_x <= 1.0:
+ centering_x = 0.5
+ if not 0.0 <= centering_y <= 1.0:
+ centering_y = 0.5
+
+ if not 0.0 <= bleed < 0.5:
+ bleed = 0.0
+
+ # calculate the area to use for resizing and cropping, subtracting
+ # the 'bleed' around the edges
+
+ # number of pixels to trim off on Top and Bottom, Left and Right
+ bleed_pixels = (bleed * image.size[0], bleed * image.size[1])
+
+ live_size = (
+ image.size[0] - bleed_pixels[0] * 2,
+ image.size[1] - bleed_pixels[1] * 2,
+ )
+
+ # calculate the aspect ratio of the live_size
+ live_size_ratio = live_size[0] / live_size[1]
+
+ # calculate the aspect ratio of the output image
+ output_ratio = size[0] / size[1]
+
+ # figure out if the sides or top/bottom will be cropped off
+ if live_size_ratio == output_ratio:
+ # live_size is already the needed ratio
+ crop_width = live_size[0]
+ crop_height = live_size[1]
+ elif live_size_ratio >= output_ratio:
+ # live_size is wider than what's needed, crop the sides
+ crop_width = output_ratio * live_size[1]
+ crop_height = live_size[1]
+ else:
+ # live_size is taller than what's needed, crop the top and bottom
+ crop_width = live_size[0]
+ crop_height = live_size[0] / output_ratio
+
+ # make the crop
+ crop_left = bleed_pixels[0] + (live_size[0] - crop_width) * centering_x
+ crop_top = bleed_pixels[1] + (live_size[1] - crop_height) * centering_y
+
+ crop = (crop_left, crop_top, crop_left + crop_width, crop_top + crop_height)
+
+ # resize the image and return it
+ return image.resize(size, method, box=crop)
+
+
+def flip(image: Image.Image) -> Image.Image:
+ """
+ Flip the image vertically (top to bottom).
+
+ :param image: The image to flip.
+ :return: An image.
+ """
+ return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
+
+
+def grayscale(image: Image.Image) -> Image.Image:
+ """
+ Convert the image to grayscale.
+
+ :param image: The image to convert.
+ :return: An image.
+ """
+ return image.convert("L")
+
+
+def invert(image: Image.Image) -> Image.Image:
+ """
+ Invert (negate) the image.
+
+ :param image: The image to invert.
+ :return: An image.
+ """
+ lut = list(range(255, -1, -1))
+ return image.point(lut) if image.mode == "1" else _lut(image, lut)
+
+
+def mirror(image: Image.Image) -> Image.Image:
+ """
+ Flip image horizontally (left to right).
+
+ :param image: The image to mirror.
+ :return: An image.
+ """
+ return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+
+
+def posterize(image: Image.Image, bits: int) -> Image.Image:
+ """
+ Reduce the number of bits for each color channel.
+
+ :param image: The image to posterize.
+ :param bits: The number of bits to keep for each channel (1-8).
+ :return: An image.
+ """
+ mask = ~(2 ** (8 - bits) - 1)
+ lut = [i & mask for i in range(256)]
+ return _lut(image, lut)
+
+
+def solarize(image: Image.Image, threshold: int = 128) -> Image.Image:
+ """
+ Invert all pixel values above a threshold.
+
+ :param image: The image to solarize.
+ :param threshold: All pixels above this grayscale level are inverted.
+ :return: An image.
+ """
+ lut = []
+ for i in range(256):
+ if i < threshold:
+ lut.append(i)
+ else:
+ lut.append(255 - i)
+ return _lut(image, lut)
+
+
+@overload
+def exif_transpose(image: Image.Image, *, in_place: Literal[True]) -> None: ...
+
+
+@overload
+def exif_transpose(
+ image: Image.Image, *, in_place: Literal[False] = False
+) -> Image.Image: ...
+
+
+def exif_transpose(image: Image.Image, *, in_place: bool = False) -> Image.Image | None:
+ """
+ If an image has an EXIF Orientation tag, other than 1, transpose the image
+ accordingly, and remove the orientation data.
+
+ :param image: The image to transpose.
+ :param in_place: Boolean. Keyword-only argument.
+ If ``True``, the original image is modified in-place, and ``None`` is returned.
+ If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned
+ with the transposition applied. If there is no transposition, a copy of the
+ image will be returned.
+ """
+ image.load()
+ image_exif = image.getexif()
+ orientation = image_exif.get(ExifTags.Base.Orientation, 1)
+ method = {
+ 2: Image.Transpose.FLIP_LEFT_RIGHT,
+ 3: Image.Transpose.ROTATE_180,
+ 4: Image.Transpose.FLIP_TOP_BOTTOM,
+ 5: Image.Transpose.TRANSPOSE,
+ 6: Image.Transpose.ROTATE_270,
+ 7: Image.Transpose.TRANSVERSE,
+ 8: Image.Transpose.ROTATE_90,
+ }.get(orientation)
+ if method is not None:
+ if in_place:
+ image.im = image.im.transpose(method)
+ image._size = image.im.size
+ else:
+ transposed_image = image.transpose(method)
+ exif_image = image if in_place else transposed_image
+
+ exif = exif_image.getexif()
+ if ExifTags.Base.Orientation in exif:
+ del exif[ExifTags.Base.Orientation]
+ if "exif" in exif_image.info:
+ exif_image.info["exif"] = exif.tobytes()
+ elif "Raw profile type exif" in exif_image.info:
+ exif_image.info["Raw profile type exif"] = exif.tobytes().hex()
+ for key in ("XML:com.adobe.xmp", "xmp"):
+ if key in exif_image.info:
+ for pattern in (
+ r'tiff:Orientation="([0-9])"',
+ r"([0-9])",
+ ):
+ value = exif_image.info[key]
+ if isinstance(value, str):
+ value = re.sub(pattern, "", value)
+ elif isinstance(value, tuple):
+ value = tuple(
+ re.sub(pattern.encode(), b"", v) for v in value
+ )
+ else:
+ value = re.sub(pattern.encode(), b"", value)
+ exif_image.info[key] = value
+ if not in_place:
+ return transposed_image
+ elif not in_place:
+ return image.copy()
+ return None
diff --git a/venv/lib/python3.11/site-packages/PIL/ImagePalette.py b/venv/lib/python3.11/site-packages/PIL/ImagePalette.py
new file mode 100644
index 000000000..2abbd46ea
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImagePalette.py
@@ -0,0 +1,290 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# image palette object
+#
+# History:
+# 1996-03-11 fl Rewritten.
+# 1997-01-03 fl Up and running.
+# 1997-08-23 fl Added load hack
+# 2001-04-16 fl Fixed randint shadow bug in random()
+#
+# Copyright (c) 1997-2001 by Secret Labs AB
+# Copyright (c) 1996-1997 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import array
+from collections.abc import Sequence
+from typing import IO
+
+from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from . import Image
+
+
+class ImagePalette:
+ """
+ Color palette for palette mapped images
+
+ :param mode: The mode to use for the palette. See:
+ :ref:`concept-modes`. Defaults to "RGB"
+ :param palette: An optional palette. If given, it must be a bytearray,
+ an array or a list of ints between 0-255. The list must consist of
+ all channels for one color followed by the next color (e.g. RGBRGBRGB).
+ Defaults to an empty palette.
+ """
+
+ def __init__(
+ self,
+ mode: str = "RGB",
+ palette: Sequence[int] | bytes | bytearray | None = None,
+ ) -> None:
+ self.mode = mode
+ self.rawmode: str | None = None # if set, palette contains raw data
+ self.palette = palette or bytearray()
+ self.dirty: int | None = None
+
+ @property
+ def palette(self) -> Sequence[int] | bytes | bytearray:
+ return self._palette
+
+ @palette.setter
+ def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
+ self._colors: dict[tuple[int, ...], int] | None = None
+ self._palette = palette
+
+ @property
+ def colors(self) -> dict[tuple[int, ...], int]:
+ if self._colors is None:
+ mode_len = len(self.mode)
+ self._colors = {}
+ for i in range(0, len(self.palette), mode_len):
+ color = tuple(self.palette[i : i + mode_len])
+ if color in self._colors:
+ continue
+ self._colors[color] = i // mode_len
+ return self._colors
+
+ @colors.setter
+ def colors(self, colors: dict[tuple[int, ...], int]) -> None:
+ self._colors = colors
+
+ def copy(self) -> ImagePalette:
+ new = ImagePalette()
+
+ new.mode = self.mode
+ new.rawmode = self.rawmode
+ if self.palette is not None:
+ new.palette = self.palette[:]
+ new.dirty = self.dirty
+
+ return new
+
+ def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]:
+ """
+ Get palette contents in format suitable for the low-level
+ ``im.putpalette`` primitive.
+
+ .. warning:: This method is experimental.
+ """
+ if self.rawmode:
+ return self.rawmode, self.palette
+ return self.mode, self.tobytes()
+
+ def tobytes(self) -> bytes:
+ """Convert palette to bytes.
+
+ .. warning:: This method is experimental.
+ """
+ if self.rawmode:
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
+ if isinstance(self.palette, bytes):
+ return self.palette
+ arr = array.array("B", self.palette)
+ return arr.tobytes()
+
+ # Declare tostring as an alias for tobytes
+ tostring = tobytes
+
+ def _new_color_index(
+ self, image: Image.Image | None = None, e: Exception | None = None
+ ) -> int:
+ if not isinstance(self.palette, bytearray):
+ self._palette = bytearray(self.palette)
+ index = len(self.palette) // len(self.mode)
+ special_colors: tuple[int | tuple[int, ...] | None, ...] = ()
+ if image:
+ special_colors = (
+ image.info.get("background"),
+ image.info.get("transparency"),
+ )
+ while index in special_colors:
+ index += 1
+ if index >= 256:
+ if image:
+ # Search for an unused index
+ for i, count in reversed(list(enumerate(image.histogram()))):
+ if count == 0 and i not in special_colors:
+ index = i
+ break
+ if index >= 256:
+ msg = "cannot allocate more than 256 colors"
+ raise ValueError(msg) from e
+ return index
+
+ def getcolor(
+ self,
+ color: tuple[int, ...],
+ image: Image.Image | None = None,
+ ) -> int:
+ """Given an rgb tuple, allocate palette entry.
+
+ .. warning:: This method is experimental.
+ """
+ if self.rawmode:
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
+ if isinstance(color, tuple):
+ if self.mode == "RGB":
+ if len(color) == 4:
+ if color[3] != 255:
+ msg = "cannot add non-opaque RGBA color to RGB palette"
+ raise ValueError(msg)
+ color = color[:3]
+ elif self.mode == "RGBA":
+ if len(color) == 3:
+ color += (255,)
+ try:
+ return self.colors[color]
+ except KeyError as e:
+ # allocate new color slot
+ index = self._new_color_index(image, e)
+ assert isinstance(self._palette, bytearray)
+ self.colors[color] = index
+ mode_len = len(self.mode)
+ if index * mode_len < len(self.palette):
+ self._palette = (
+ self._palette[: index * mode_len]
+ + bytes(color)
+ + self._palette[index * mode_len + mode_len :]
+ )
+ else:
+ self._palette += bytes(color)
+ self.dirty = 1
+ return index
+ else:
+ msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
+ raise ValueError(msg)
+
+ def save(self, fp: str | IO[str]) -> None:
+ """Save palette to text file.
+
+ .. warning:: This method is experimental.
+ """
+ if self.rawmode:
+ msg = "palette contains raw palette data"
+ raise ValueError(msg)
+ open_fp = False
+ if isinstance(fp, str):
+ fp = open(fp, "w")
+ open_fp = True
+ try:
+ fp.write("# Palette\n")
+ fp.write(f"# Mode: {self.mode}\n")
+ palette_len = len(self.palette)
+ for i in range(256):
+ fp.write(f"{i}")
+ for j in range(i * len(self.mode), (i + 1) * len(self.mode)):
+ fp.write(f" {self.palette[j] if j < palette_len else 0}")
+ fp.write("\n")
+ finally:
+ if open_fp:
+ fp.close()
+
+
+# --------------------------------------------------------------------
+# Internal
+
+
+def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
+ palette = ImagePalette()
+ palette.rawmode = rawmode
+ palette.palette = data
+ palette.dirty = 1
+ return palette
+
+
+# --------------------------------------------------------------------
+# Factories
+
+
+def make_linear_lut(black: int, white: float) -> list[int]:
+ if black == 0:
+ return [int(white * i // 255) for i in range(256)]
+
+ msg = "unavailable when black is non-zero"
+ raise NotImplementedError(msg) # FIXME
+
+
+def make_gamma_lut(exp: float) -> list[int]:
+ return [int(((i / 255.0) ** exp) * 255.0 + 0.5) for i in range(256)]
+
+
+def negative(mode: str = "RGB") -> ImagePalette:
+ palette = list(range(256 * len(mode)))
+ palette.reverse()
+ return ImagePalette(mode, [i // len(mode) for i in palette])
+
+
+def random(mode: str = "RGB") -> ImagePalette:
+ from random import randint
+
+ palette = [randint(0, 255) for _ in range(256 * len(mode))]
+ return ImagePalette(mode, palette)
+
+
+def sepia(white: str = "#fff0c0") -> ImagePalette:
+ bands = [make_linear_lut(0, band) for band in ImageColor.getrgb(white)]
+ return ImagePalette("RGB", [bands[i % 3][i // 3] for i in range(256 * 3)])
+
+
+def wedge(mode: str = "RGB") -> ImagePalette:
+ palette = list(range(256 * len(mode)))
+ return ImagePalette(mode, [i // len(mode) for i in palette])
+
+
+def load(filename: str) -> tuple[bytes, str]:
+ # FIXME: supports GIMP gradients only
+
+ with open(filename, "rb") as fp:
+ paletteHandlers: list[
+ type[
+ GimpPaletteFile.GimpPaletteFile
+ | GimpGradientFile.GimpGradientFile
+ | PaletteFile.PaletteFile
+ ]
+ ] = [
+ GimpPaletteFile.GimpPaletteFile,
+ GimpGradientFile.GimpGradientFile,
+ PaletteFile.PaletteFile,
+ ]
+ for paletteHandler in paletteHandlers:
+ try:
+ fp.seek(0)
+ lut = paletteHandler(fp).getpalette()
+ if lut:
+ break
+ except (SyntaxError, ValueError):
+ pass
+ else:
+ msg = "cannot load palette"
+ raise OSError(msg)
+
+ return lut # data, rawmode
diff --git a/venv/lib/python3.11/site-packages/PIL/ImagePath.py b/venv/lib/python3.11/site-packages/PIL/ImagePath.py
new file mode 100644
index 000000000..77e8a609a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImagePath.py
@@ -0,0 +1,20 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# path interface
+#
+# History:
+# 1996-11-04 fl Created
+# 2002-04-14 fl Added documentation stub class
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1996.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from . import Image
+
+Path = Image.core.path
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageQt.py b/venv/lib/python3.11/site-packages/PIL/ImageQt.py
new file mode 100644
index 000000000..af4d0742d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageQt.py
@@ -0,0 +1,219 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# a simple Qt image interface.
+#
+# history:
+# 2006-06-03 fl: created
+# 2006-06-04 fl: inherit from QImage instead of wrapping it
+# 2006-06-05 fl: removed toimage helper; move string support to ImageQt
+# 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com)
+#
+# Copyright (c) 2006 by Secret Labs AB
+# Copyright (c) 2006 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import sys
+from io import BytesIO
+
+from . import Image
+from ._util import is_path
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Any
+
+ from . import ImageFile
+
+ QBuffer: type
+
+qt_version: str | None
+qt_versions = [
+ ["6", "PyQt6"],
+ ["side6", "PySide6"],
+]
+
+# If a version has already been imported, attempt it first
+qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
+for version, qt_module in qt_versions:
+ try:
+ qRgba: Callable[[int, int, int, int], int]
+ if qt_module == "PyQt6":
+ from PyQt6.QtCore import QBuffer, QByteArray, QIODevice
+ from PyQt6.QtGui import QImage, QPixmap, qRgba
+ elif qt_module == "PySide6":
+ from PySide6.QtCore import ( # type: ignore[assignment]
+ QBuffer,
+ QByteArray,
+ QIODevice,
+ )
+ from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment]
+ except (ImportError, RuntimeError):
+ continue
+ qt_is_installed = True
+ qt_version = version
+ break
+else:
+ qt_is_installed = False
+ qt_version = None
+
+
+def rgb(r: int, g: int, b: int, a: int = 255) -> int:
+ """(Internal) Turns an RGB color into a Qt compatible color integer."""
+ # use qRgb to pack the colors, and then turn the resulting long
+ # into a negative integer with the same bitpattern.
+ return qRgba(r, g, b, a) & 0xFFFFFFFF
+
+
+def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile:
+ """
+ :param im: QImage or PIL ImageQt object
+ """
+ buffer = QBuffer()
+ qt_openmode: object
+ if qt_version == "6":
+ try:
+ qt_openmode = getattr(QIODevice, "OpenModeFlag")
+ except AttributeError:
+ qt_openmode = getattr(QIODevice, "OpenMode")
+ else:
+ qt_openmode = QIODevice
+ buffer.open(getattr(qt_openmode, "ReadWrite"))
+ # preserve alpha channel with png
+ # otherwise ppm is more friendly with Image.open
+ if im.hasAlphaChannel():
+ im.save(buffer, "png")
+ else:
+ im.save(buffer, "ppm")
+
+ b = BytesIO()
+ b.write(buffer.data())
+ buffer.close()
+ b.seek(0)
+
+ return Image.open(b)
+
+
+def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile:
+ return fromqimage(im)
+
+
+def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
+ """
+ converts each scanline of data from 8 bit to 32 bit aligned
+ """
+
+ bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode]
+
+ # calculate bytes per line and the extra padding if needed
+ bits_per_line = bits_per_pixel * width
+ full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8)
+ bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0)
+
+ extra_padding = -bytes_per_line % 4
+
+ # already 32 bit aligned by luck
+ if not extra_padding:
+ return bytes
+
+ new_data = [
+ bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding
+ for i in range(len(bytes) // bytes_per_line)
+ ]
+
+ return b"".join(new_data)
+
+
+def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
+ data = None
+ colortable = None
+ exclusive_fp = False
+
+ # handle filename, if given instead of image name
+ if hasattr(im, "toUtf8"):
+ # FIXME - is this really the best way to do this?
+ im = str(im.toUtf8(), "utf-8")
+ if is_path(im):
+ im = Image.open(im)
+ exclusive_fp = True
+ assert isinstance(im, Image.Image)
+
+ qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage
+ if im.mode == "1":
+ format = getattr(qt_format, "Format_Mono")
+ elif im.mode == "L":
+ format = getattr(qt_format, "Format_Indexed8")
+ colortable = [rgb(i, i, i) for i in range(256)]
+ elif im.mode == "P":
+ format = getattr(qt_format, "Format_Indexed8")
+ palette = im.getpalette()
+ assert palette is not None
+ colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
+ elif im.mode == "RGB":
+ # Populate the 4th channel with 255
+ im = im.convert("RGBA")
+
+ data = im.tobytes("raw", "BGRA")
+ format = getattr(qt_format, "Format_RGB32")
+ elif im.mode == "RGBA":
+ data = im.tobytes("raw", "BGRA")
+ format = getattr(qt_format, "Format_ARGB32")
+ elif im.mode == "I;16":
+ im = im.point(lambda i: i * 256)
+
+ format = getattr(qt_format, "Format_Grayscale16")
+ else:
+ if exclusive_fp:
+ im.close()
+ msg = f"unsupported image mode {repr(im.mode)}"
+ raise ValueError(msg)
+
+ size = im.size
+ __data = data or align8to32(im.tobytes(), size[0], im.mode)
+ if exclusive_fp:
+ im.close()
+ return {"data": __data, "size": size, "format": format, "colortable": colortable}
+
+
+if qt_is_installed:
+
+ class ImageQt(QImage):
+ def __init__(self, im: Image.Image | str | QByteArray) -> None:
+ """
+ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
+ class.
+
+ :param im: A PIL Image object, or a file name (given either as
+ Python string or a PyQt string object).
+ """
+ im_data = _toqclass_helper(im)
+ # must keep a reference, or Qt will crash!
+ # All QImage constructors that take data operate on an existing
+ # buffer, so this buffer has to hang on for the life of the image.
+ # Fixes https://github.com/python-pillow/Pillow/issues/1370
+ self.__data = im_data["data"]
+ super().__init__(
+ self.__data,
+ im_data["size"][0],
+ im_data["size"][1],
+ im_data["format"],
+ )
+ if im_data["colortable"]:
+ self.setColorTable(im_data["colortable"])
+
+
+def toqimage(im: Image.Image | str | QByteArray) -> ImageQt:
+ return ImageQt(im)
+
+
+def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap:
+ qimage = toqimage(im)
+ pixmap = getattr(QPixmap, "fromImage")(qimage)
+ if qt_version == "6":
+ pixmap.detach()
+ return pixmap
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageSequence.py b/venv/lib/python3.11/site-packages/PIL/ImageSequence.py
new file mode 100644
index 000000000..361be4897
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageSequence.py
@@ -0,0 +1,88 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# sequence support classes
+#
+# history:
+# 1997-02-20 fl Created
+#
+# Copyright (c) 1997 by Secret Labs AB.
+# Copyright (c) 1997 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+##
+from __future__ import annotations
+
+from . import Image
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
+
+class Iterator:
+ """
+ This class implements an iterator object that can be used to loop
+ over an image sequence.
+
+ You can use the ``[]`` operator to access elements by index. This operator
+ will raise an :py:exc:`IndexError` if you try to access a nonexistent
+ frame.
+
+ :param im: An image object.
+ """
+
+ def __init__(self, im: Image.Image) -> None:
+ if not hasattr(im, "seek"):
+ msg = "im must have seek method"
+ raise AttributeError(msg)
+ self.im = im
+ self.position = getattr(self.im, "_min_frame", 0)
+
+ def __getitem__(self, ix: int) -> Image.Image:
+ try:
+ self.im.seek(ix)
+ return self.im
+ except EOFError as e:
+ msg = "end of sequence"
+ raise IndexError(msg) from e
+
+ def __iter__(self) -> Iterator:
+ return self
+
+ def __next__(self) -> Image.Image:
+ try:
+ self.im.seek(self.position)
+ self.position += 1
+ return self.im
+ except EOFError as e:
+ msg = "end of sequence"
+ raise StopIteration(msg) from e
+
+
+def all_frames(
+ im: Image.Image | list[Image.Image],
+ func: Callable[[Image.Image], Image.Image] | None = None,
+) -> list[Image.Image]:
+ """
+ Applies a given function to all frames in an image or a list of images.
+ The frames are returned as a list of separate images.
+
+ :param im: An image, or a list of images.
+ :param func: The function to apply to all of the image frames.
+ :returns: A list of images.
+ """
+ if not isinstance(im, list):
+ im = [im]
+
+ ims = []
+ for imSequence in im:
+ current = imSequence.tell()
+
+ ims += [im_frame.copy() for im_frame in Iterator(imSequence)]
+
+ imSequence.seek(current)
+ return [func(im) for im in ims] if func else ims
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageShow.py b/venv/lib/python3.11/site-packages/PIL/ImageShow.py
new file mode 100644
index 000000000..dd8aa0d36
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageShow.py
@@ -0,0 +1,367 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# im.show() drivers
+#
+# History:
+# 2008-04-06 fl Created
+#
+# Copyright (c) Secret Labs AB 2008.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import abc
+import os
+import shutil
+import subprocess
+import sys
+from shlex import quote
+from typing import Any
+
+from . import Image
+
+_viewers: list[Viewer] = []
+
+
+def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None:
+ """
+ The :py:func:`register` function is used to register additional viewers::
+
+ from PIL import ImageShow
+ ImageShow.register(MyViewer()) # MyViewer will be used as a last resort
+ ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised
+ ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised
+
+ :param viewer: The viewer to be registered.
+ :param order:
+ Zero or a negative integer to prepend this viewer to the list,
+ a positive integer to append it.
+ """
+ if isinstance(viewer, type) and issubclass(viewer, Viewer):
+ viewer = viewer()
+ if order > 0:
+ _viewers.append(viewer)
+ else:
+ _viewers.insert(0, viewer)
+
+
+def show(image: Image.Image, title: str | None = None, **options: Any) -> bool:
+ r"""
+ Display a given image.
+
+ :param image: An image object.
+ :param title: Optional title. Not all viewers can display the title.
+ :param \**options: Additional viewer options.
+ :returns: ``True`` if a suitable viewer was found, ``False`` otherwise.
+ """
+ for viewer in _viewers:
+ if viewer.show(image, title=title, **options):
+ return True
+ return False
+
+
+class Viewer:
+ """Base class for viewers."""
+
+ # main api
+
+ def show(self, image: Image.Image, **options: Any) -> int:
+ """
+ The main function for displaying an image.
+ Converts the given image to the target format and displays it.
+ """
+
+ if not (
+ image.mode in ("1", "RGBA")
+ or (self.format == "PNG" and image.mode in ("I;16", "LA"))
+ ):
+ base = Image.getmodebase(image.mode)
+ if image.mode != base:
+ image = image.convert(base)
+
+ return self.show_image(image, **options)
+
+ # hook methods
+
+ format: str | None = None
+ """The format to convert the image into."""
+ options: dict[str, Any] = {}
+ """Additional options used to convert the image."""
+
+ def get_format(self, image: Image.Image) -> str | None:
+ """Return format name, or ``None`` to save as PGM/PPM."""
+ return self.format
+
+ def get_command(self, file: str, **options: Any) -> str:
+ """
+ Returns the command used to display the file.
+ Not implemented in the base class.
+ """
+ msg = "unavailable in base viewer"
+ raise NotImplementedError(msg)
+
+ def save_image(self, image: Image.Image) -> str:
+ """Save to temporary file and return filename."""
+ return image._dump(format=self.get_format(image), **self.options)
+
+ def show_image(self, image: Image.Image, **options: Any) -> int:
+ """Display the given image."""
+ return self.show_file(self.save_image(image), **options)
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ os.system(self.get_command(path, **options)) # nosec
+ return 1
+
+ def _remove_path_after_delay(self, path: str) -> None:
+ pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
+ executable = (not pyinstaller and sys.executable) or shutil.which("python3")
+
+ if executable:
+ subprocess.Popen(
+ [
+ executable,
+ "-c",
+ "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])",
+ path,
+ ]
+ )
+
+
+# --------------------------------------------------------------------
+
+
+class WindowsViewer(Viewer):
+ """The default viewer on Windows is the default system application for PNG files."""
+
+ format = "PNG"
+ options = {"compress_level": 1, "save_all": True}
+
+ def get_command(self, file: str, **options: Any) -> str:
+ if '"' in file:
+ msg = "Windows filenames cannot contain double quotes"
+ raise ValueError(msg)
+ file = file.replace("%", '"%"')
+ return (
+ f'start "Pillow" /WAIT "{file}" '
+ "&& ping -n 4 127.0.0.1 >NUL "
+ f'&& del /f "{file}"'
+ )
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ if sys.platform == "win32":
+ os.startfile(path)
+ self._remove_path_after_delay(path)
+ return 1
+
+
+if sys.platform == "win32":
+ register(WindowsViewer)
+
+
+class MacViewer(Viewer):
+ """The default viewer on macOS using ``Preview.app``."""
+
+ format = "PNG"
+ options = {"compress_level": 1, "save_all": True}
+
+ def get_command(self, file: str, **options: Any) -> str:
+ # on darwin open returns immediately resulting in the temp
+ # file removal while app is opening
+ command = "open -a Preview.app"
+ command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
+ return command
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ subprocess.call(["open", "-a", "Preview.app", path])
+ self._remove_path_after_delay(path)
+ return 1
+
+
+if sys.platform == "darwin":
+ register(MacViewer)
+
+
+class UnixViewer(abc.ABC, Viewer):
+ format = "PNG"
+ options = {"compress_level": 1, "save_all": True}
+
+ @abc.abstractmethod
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
+ pass
+
+ def get_command(self, file: str, **options: Any) -> str:
+ command = self.get_command_ex(file, **options)[0]
+ return f"{command} {quote(file)}"
+
+
+class XDGViewer(UnixViewer):
+ """
+ The freedesktop.org ``xdg-open`` command.
+ """
+
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
+ command = executable = "xdg-open"
+ return command, executable
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ subprocess.Popen(["xdg-open", path])
+ return 1
+
+
+class DisplayViewer(UnixViewer):
+ """
+ The ImageMagick ``display`` command.
+ This viewer supports the ``title`` parameter.
+ """
+
+ def get_command_ex(
+ self, file: str, title: str | None = None, **options: Any
+ ) -> tuple[str, str]:
+ command = executable = "display"
+ if title:
+ command += f" -title {quote(title)}"
+ return command, executable
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ args = ["display"]
+ title = options.get("title")
+ if title:
+ args += ["-title", title]
+ args.append(path)
+
+ subprocess.Popen(args)
+ return 1
+
+
+class GmDisplayViewer(UnixViewer):
+ """The GraphicsMagick ``gm display`` command."""
+
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
+ executable = "gm"
+ command = "gm display"
+ return command, executable
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ subprocess.Popen(["gm", "display", path])
+ return 1
+
+
+class EogViewer(UnixViewer):
+ """The GNOME Image Viewer ``eog`` command."""
+
+ def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
+ executable = "eog"
+ command = "eog -n"
+ return command, executable
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ subprocess.Popen(["eog", "-n", path])
+ return 1
+
+
+class XVViewer(UnixViewer):
+ """
+ The X Viewer ``xv`` command.
+ This viewer supports the ``title`` parameter.
+ """
+
+ def get_command_ex(
+ self, file: str, title: str | None = None, **options: Any
+ ) -> tuple[str, str]:
+ # note: xv is pretty outdated. most modern systems have
+ # imagemagick's display command instead.
+ command = executable = "xv"
+ if title:
+ command += f" -name {quote(title)}"
+ return command, executable
+
+ def show_file(self, path: str, **options: Any) -> int:
+ """
+ Display given file.
+ """
+ if not os.path.exists(path):
+ raise FileNotFoundError
+ args = ["xv"]
+ title = options.get("title")
+ if title:
+ args += ["-name", title]
+ args.append(path)
+
+ subprocess.Popen(args)
+ return 1
+
+
+if sys.platform not in ("win32", "darwin"): # unixoids
+ if shutil.which("xdg-open"):
+ register(XDGViewer)
+ if shutil.which("display"):
+ register(DisplayViewer)
+ if shutil.which("gm"):
+ register(GmDisplayViewer)
+ if shutil.which("eog"):
+ register(EogViewer)
+ if shutil.which("xv"):
+ register(XVViewer)
+
+
+class IPythonViewer(Viewer):
+ """The viewer for IPython frontends."""
+
+ def show_image(self, image: Image.Image, **options: Any) -> int:
+ ipython_display(image)
+ return 1
+
+
+try:
+ from IPython.display import display as ipython_display
+except ImportError:
+ pass
+else:
+ register(IPythonViewer)
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print("Syntax: python3 ImageShow.py imagefile [title]")
+ sys.exit()
+
+ with Image.open(sys.argv[1]) as im:
+ print(show(im, *sys.argv[2:]))
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageStat.py b/venv/lib/python3.11/site-packages/PIL/ImageStat.py
new file mode 100644
index 000000000..3a1044ba4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageStat.py
@@ -0,0 +1,167 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# global image statistics
+#
+# History:
+# 1996-04-05 fl Created
+# 1997-05-21 fl Added mask; added rms, var, stddev attributes
+# 1997-08-05 fl Added median
+# 1998-07-05 hk Fixed integer overflow error
+#
+# Notes:
+# This class shows how to implement delayed evaluation of attributes.
+# To get a certain value, simply access the corresponding attribute.
+# The __getattr__ dispatcher takes care of the rest.
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1996-97.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import math
+from functools import cached_property
+
+from . import Image
+
+
+class Stat:
+ def __init__(
+ self, image_or_list: Image.Image | list[int], mask: Image.Image | None = None
+ ) -> None:
+ """
+ Calculate statistics for the given image. If a mask is included,
+ only the regions covered by that mask are included in the
+ statistics. You can also pass in a previously calculated histogram.
+
+ :param image: A PIL image, or a precalculated histogram.
+
+ .. note::
+
+ For a PIL image, calculations rely on the
+ :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are
+ grouped into 256 bins, even if the image has more than 8 bits per
+ channel. So ``I`` and ``F`` mode images have a maximum ``mean``,
+ ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum
+ of more than 255.
+
+ :param mask: An optional mask.
+ """
+ if isinstance(image_or_list, Image.Image):
+ self.h = image_or_list.histogram(mask)
+ elif isinstance(image_or_list, list):
+ self.h = image_or_list
+ else:
+ msg = "first argument must be image or list" # type: ignore[unreachable]
+ raise TypeError(msg)
+ self.bands = list(range(len(self.h) // 256))
+
+ @cached_property
+ def extrema(self) -> list[tuple[int, int]]:
+ """
+ Min/max values for each band in the image.
+
+ .. note::
+ This relies on the :py:meth:`~PIL.Image.Image.histogram` method, and
+ simply returns the low and high bins used. This is correct for
+ images with 8 bits per channel, but fails for other modes such as
+ ``I`` or ``F``. Instead, use :py:meth:`~PIL.Image.Image.getextrema` to
+ return per-band extrema for the image. This is more correct and
+ efficient because, for non-8-bit modes, the histogram method uses
+ :py:meth:`~PIL.Image.Image.getextrema` to determine the bins used.
+ """
+
+ def minmax(histogram: list[int]) -> tuple[int, int]:
+ res_min, res_max = 255, 0
+ for i in range(256):
+ if histogram[i]:
+ res_min = i
+ break
+ for i in range(255, -1, -1):
+ if histogram[i]:
+ res_max = i
+ break
+ return res_min, res_max
+
+ return [minmax(self.h[i:]) for i in range(0, len(self.h), 256)]
+
+ @cached_property
+ def count(self) -> list[int]:
+ """Total number of pixels for each band in the image."""
+ return [sum(self.h[i : i + 256]) for i in range(0, len(self.h), 256)]
+
+ @cached_property
+ def sum(self) -> list[float]:
+ """Sum of all pixels for each band in the image."""
+
+ v = []
+ for i in range(0, len(self.h), 256):
+ layer_sum = 0.0
+ for j in range(256):
+ layer_sum += j * self.h[i + j]
+ v.append(layer_sum)
+ return v
+
+ @cached_property
+ def sum2(self) -> list[float]:
+ """Squared sum of all pixels for each band in the image."""
+
+ v = []
+ for i in range(0, len(self.h), 256):
+ sum2 = 0.0
+ for j in range(256):
+ sum2 += (j**2) * float(self.h[i + j])
+ v.append(sum2)
+ return v
+
+ @cached_property
+ def mean(self) -> list[float]:
+ """Average (arithmetic mean) pixel level for each band in the image."""
+ return [self.sum[i] / self.count[i] if self.count[i] else 0 for i in self.bands]
+
+ @cached_property
+ def median(self) -> list[int]:
+ """Median pixel level for each band in the image."""
+
+ v = []
+ for i in self.bands:
+ s = 0
+ half = self.count[i] // 2
+ b = i * 256
+ for j in range(256):
+ s = s + self.h[b + j]
+ if s > half:
+ break
+ v.append(j)
+ return v
+
+ @cached_property
+ def rms(self) -> list[float]:
+ """RMS (root-mean-square) for each band in the image."""
+ return [
+ math.sqrt(self.sum2[i] / self.count[i]) if self.count[i] else 0
+ for i in self.bands
+ ]
+
+ @cached_property
+ def var(self) -> list[float]:
+ """Variance for each band in the image."""
+ return [
+ (
+ (self.sum2[i] - (self.sum[i] ** 2.0) / self.count[i]) / self.count[i]
+ if self.count[i]
+ else 0
+ )
+ for i in self.bands
+ ]
+
+ @cached_property
+ def stddev(self) -> list[float]:
+ """Standard deviation for each band in the image."""
+ return [math.sqrt(self.var[i]) for i in self.bands]
+
+
+Global = Stat # compatibility
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageText.py b/venv/lib/python3.11/site-packages/PIL/ImageText.py
new file mode 100644
index 000000000..96601813c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageText.py
@@ -0,0 +1,509 @@
+from __future__ import annotations
+
+import math
+import re
+from typing import AnyStr, Generic, NamedTuple
+
+from . import ImageFont
+from ._typing import _Ink
+
+
+class _Line(NamedTuple):
+ x: float
+ y: float
+ anchor: str
+ text: str | bytes
+
+
+class _Wrap(Generic[AnyStr]):
+ lines: list[AnyStr] = []
+ position = 0
+ offset = 0
+
+ def __init__(
+ self,
+ text: Text[AnyStr],
+ width: int,
+ height: int | None = None,
+ font: ImageFont.BaseImageFont | None = None,
+ ) -> None:
+ self.text: Text[AnyStr] = text
+ self.width = width
+ self.height = height
+ self.font = font
+
+ input_text = self.text.text
+ emptystring = "" if isinstance(input_text, str) else b""
+ line = emptystring
+
+ for word in re.findall(
+ r"\s*\S+" if isinstance(input_text, str) else rb"\s*\S+", input_text
+ ):
+ newlines = re.findall(
+ r"[^\S\n]*\n" if isinstance(input_text, str) else rb"[^\S\n]*\n", word
+ )
+ if newlines:
+ if not self.add_line(line):
+ break
+ for i, line in enumerate(newlines):
+ if i != 0 and not self.add_line(emptystring):
+ break
+ self.position += len(line)
+ word = word[len(line) :]
+ line = emptystring
+
+ new_line = line + word
+ if self.text._get_bbox(new_line, self.font)[2] <= width:
+ # This word fits on the line
+ line = new_line
+ continue
+
+ # This word does not fit on the line
+ if line and not self.add_line(line):
+ break
+
+ original_length = len(word)
+ word = word.lstrip()
+ self.offset = original_length - len(word)
+
+ if self.text._get_bbox(word, self.font)[2] > width:
+ if font is None:
+ msg = "Word does not fit within line"
+ raise ValueError(msg)
+ break
+ line = word
+ else:
+ if line:
+ self.add_line(line)
+ self.remaining_text: AnyStr = input_text[self.position :]
+
+ def add_line(self, line: AnyStr) -> bool:
+ lines = self.lines + [line]
+ if self.height is not None:
+ last_line_y = self.text._split(lines=lines)[-1].y
+ last_line_height = self.text._get_bbox(line, self.font)[3]
+ if last_line_y + last_line_height > self.height:
+ return False
+
+ self.lines = lines
+ self.position += len(line) + self.offset
+ self.offset = 0
+ return True
+
+
+class Text(Generic[AnyStr]):
+ def __init__(
+ self,
+ text: AnyStr,
+ font: ImageFont.BaseImageFont | None = None,
+ mode: str = "RGB",
+ spacing: float = 4,
+ direction: str | None = None,
+ features: list[str] | None = None,
+ language: str | None = None,
+ ) -> None:
+ """
+ :param text: String to be drawn.
+ :param font: Either an :py:class:`~PIL.ImageFont.ImageFont` instance,
+ :py:class:`~PIL.ImageFont.FreeTypeFont` instance,
+ :py:class:`~PIL.ImageFont.TransposedFont` instance or ``None``. If
+ ``None``, the default font from :py:meth:`.ImageFont.load_default`
+ will be used.
+ :param mode: The image mode this will be used with.
+ :param spacing: The number of pixels between lines.
+ :param direction: Direction of the text. It can be ``"rtl"`` (right to left),
+ ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom).
+ Requires libraqm.
+ :param features: A list of OpenType font features to be used during text
+ layout. This is usually used to turn on optional font features
+ that are not enabled by default, for example ``"dlig"`` or
+ ``"ss01"``, but can be also used to turn off default font
+ features, for example ``"-liga"`` to disable ligatures or
+ ``"-kern"`` to disable kerning. To get all supported
+ features, see `OpenType docs`_.
+ Requires libraqm.
+ :param language: Language of the text. Different languages may use
+ different glyph shapes or ligatures. This parameter tells
+ the font which language the text is in, and to apply the
+ correct substitutions as appropriate, if available.
+ It should be a `BCP 47 language code`_.
+ Requires libraqm.
+ """
+ self.text: AnyStr = text
+ self.font = font or ImageFont.load_default()
+
+ self.mode = mode
+ self.spacing = spacing
+ self.direction = direction
+ self.features = features
+ self.language = language
+
+ self.embedded_color = False
+
+ self.stroke_width: float = 0
+ self.stroke_fill: _Ink | None = None
+
+ def embed_color(self) -> None:
+ """
+ Use embedded color glyphs (COLR, CBDT, SBIX).
+ """
+ if self.mode not in ("RGB", "RGBA"):
+ msg = "Embedded color supported only in RGB and RGBA modes"
+ raise ValueError(msg)
+ self.embedded_color = True
+
+ def stroke(self, width: float = 0, fill: _Ink | None = None) -> None:
+ """
+ :param width: The width of the text stroke.
+ :param fill: Color to use for the text stroke when drawing. If not given, will
+ default to the ``fill`` parameter from
+ :py:meth:`.ImageDraw.ImageDraw.text`.
+ """
+ self.stroke_width = width
+ self.stroke_fill = fill
+
+ def _get_fontmode(self) -> str:
+ if self.mode in ("1", "P", "I", "F"):
+ return "1"
+ elif self.embedded_color:
+ return "RGBA"
+ else:
+ return "L"
+
+ def wrap(
+ self,
+ width: int,
+ height: int | None = None,
+ scaling: str | tuple[str, int] | None = None,
+ ) -> Text[AnyStr] | None:
+ """
+ Wrap text to fit within a given width.
+
+ :param width: The width to fit within.
+ :param height: An optional height limit. Any text that does not fit within this
+ will be returned as a new :py:class:`.Text` object.
+ :param scaling: An optional directive to scale the text, either "grow" as much
+ as possible within the given dimensions, or "shrink" until it
+ fits. It can also be a tuple of (direction, limit), with an
+ integer limit to stop scaling at.
+
+ :returns: An :py:class:`.Text` object, or None.
+ """
+ if isinstance(self.font, ImageFont.TransposedFont):
+ msg = "TransposedFont not supported"
+ raise ValueError(msg)
+ if self.direction not in (None, "ltr"):
+ msg = "Only ltr direction supported"
+ raise ValueError(msg)
+
+ if scaling is None:
+ wrap = _Wrap(self, width, height)
+ else:
+ if not isinstance(self.font, ImageFont.FreeTypeFont):
+ msg = "'scaling' only supports FreeTypeFont"
+ raise ValueError(msg)
+ if height is None:
+ msg = "'scaling' requires 'height'"
+ raise ValueError(msg)
+
+ if isinstance(scaling, str):
+ limit = 1
+ else:
+ scaling, limit = scaling
+
+ font = self.font
+ wrap = _Wrap(self, width, height, font)
+ if scaling == "shrink":
+ if not wrap.remaining_text:
+ return None
+
+ size = math.ceil(font.size)
+ while wrap.remaining_text:
+ if size == max(limit, 1):
+ msg = "Text could not be scaled"
+ raise ValueError(msg)
+ size -= 1
+ font = self.font.font_variant(size=size)
+ wrap = _Wrap(self, width, height, font)
+ self.font = font
+ else:
+ if wrap.remaining_text:
+ msg = "Text could not be scaled"
+ raise ValueError(msg)
+
+ size = math.floor(font.size)
+ while not wrap.remaining_text:
+ if size == limit:
+ msg = "Text could not be scaled"
+ raise ValueError(msg)
+ size += 1
+ font = self.font.font_variant(size=size)
+ last_wrap = wrap
+ wrap = _Wrap(self, width, height, font)
+ size -= 1
+ if size != self.font.size:
+ self.font = self.font.font_variant(size=size)
+ wrap = last_wrap
+
+ if wrap.remaining_text:
+ text = Text(
+ text=wrap.remaining_text,
+ font=self.font,
+ mode=self.mode,
+ spacing=self.spacing,
+ direction=self.direction,
+ features=self.features,
+ language=self.language,
+ )
+ text.embedded_color = self.embedded_color
+ text.stroke_width = self.stroke_width
+ text.stroke_fill = self.stroke_fill
+ else:
+ text = None
+
+ newline = "\n" if isinstance(self.text, str) else b"\n"
+ self.text = newline.join(wrap.lines)
+ return text
+
+ def get_length(self) -> float:
+ """
+ Returns length (in pixels with 1/64 precision) of text.
+
+ This is the amount by which following text should be offset.
+ Text bounding box may extend past the length in some fonts,
+ e.g. when using italics or accents.
+
+ The result is returned as a float; it is a whole number if using basic layout.
+
+ Note that the sum of two lengths may not equal the length of a concatenated
+ string due to kerning. If you need to adjust for kerning, include the following
+ character and subtract its length.
+
+ For example, instead of::
+
+ hello = ImageText.Text("Hello", font).get_length()
+ world = ImageText.Text("World", font).get_length()
+ helloworld = ImageText.Text("HelloWorld", font).get_length()
+ assert hello + world == helloworld
+
+ use::
+
+ hello = (
+ ImageText.Text("HelloW", font).get_length() -
+ ImageText.Text("W", font).get_length()
+ ) # adjusted for kerning
+ world = ImageText.Text("World", font).get_length()
+ helloworld = ImageText.Text("HelloWorld", font).get_length()
+ assert hello + world == helloworld
+
+ or disable kerning with (requires libraqm)::
+
+ hello = ImageText.Text("Hello", font, features=["-kern"]).get_length()
+ world = ImageText.Text("World", font, features=["-kern"]).get_length()
+ helloworld = ImageText.Text(
+ "HelloWorld", font, features=["-kern"]
+ ).get_length()
+ assert hello + world == helloworld
+
+ :return: Either width for horizontal text, or height for vertical text.
+ """
+ if isinstance(self.text, str):
+ multiline = "\n" in self.text
+ else:
+ multiline = b"\n" in self.text
+ if multiline:
+ msg = "can't measure length of multiline text"
+ raise ValueError(msg)
+ return self.font.getlength(
+ self.text,
+ self._get_fontmode(),
+ self.direction,
+ self.features,
+ self.language,
+ )
+
+ def _split(
+ self,
+ xy: tuple[float, float] = (0, 0),
+ anchor: str | None = None,
+ align: str = "left",
+ lines: list[str] | list[bytes] | None = None,
+ ) -> list[_Line]:
+ if anchor is None:
+ anchor = "lt" if self.direction == "ttb" else "la"
+ elif len(anchor) != 2:
+ msg = "anchor must be a 2 character string"
+ raise ValueError(msg)
+
+ if lines is None:
+ lines = (
+ self.text.split("\n")
+ if isinstance(self.text, str)
+ else self.text.split(b"\n")
+ )
+ if len(lines) == 1:
+ return [_Line(xy[0], xy[1], anchor, lines[0])]
+
+ if anchor[1] in "tb" and self.direction != "ttb":
+ msg = "anchor not supported for multiline text"
+ raise ValueError(msg)
+
+ fontmode = self._get_fontmode()
+ line_spacing = (
+ self.font.getbbox(
+ "A",
+ fontmode,
+ None,
+ self.features,
+ self.language,
+ self.stroke_width,
+ )[3]
+ + self.stroke_width
+ + self.spacing
+ )
+
+ top = xy[1]
+ parts = []
+ if self.direction == "ttb":
+ left = xy[0]
+ for line in lines:
+ parts.append(_Line(left, top, anchor, line))
+ left += line_spacing
+ else:
+ widths = []
+ max_width: float = 0
+ for line in lines:
+ line_width = self.font.getlength(
+ line, fontmode, self.direction, self.features, self.language
+ )
+ widths.append(line_width)
+ max_width = max(max_width, line_width)
+
+ if anchor[1] == "m":
+ top -= (len(lines) - 1) * line_spacing / 2.0
+ elif anchor[1] == "d":
+ top -= (len(lines) - 1) * line_spacing
+
+ idx = -1
+ for line in lines:
+ left = xy[0]
+ idx += 1
+ width_difference = max_width - widths[idx]
+
+ # align by align parameter
+ if align in ("left", "justify"):
+ pass
+ elif align == "center":
+ left += width_difference / 2.0
+ elif align == "right":
+ left += width_difference
+ else:
+ msg = 'align must be "left", "center", "right" or "justify"'
+ raise ValueError(msg)
+
+ if (
+ align == "justify"
+ and width_difference != 0
+ and idx != len(lines) - 1
+ ):
+ words = (
+ line.split(" ") if isinstance(line, str) else line.split(b" ")
+ )
+ if len(words) > 1:
+ # align left by anchor
+ if anchor[0] == "m":
+ left -= max_width / 2.0
+ elif anchor[0] == "r":
+ left -= max_width
+
+ word_widths = [
+ self.font.getlength(
+ word,
+ fontmode,
+ self.direction,
+ self.features,
+ self.language,
+ )
+ for word in words
+ ]
+ word_anchor = "l" + anchor[1]
+ width_difference = max_width - sum(word_widths)
+ i = 0
+ for word in words:
+ parts.append(_Line(left, top, word_anchor, word))
+ left += word_widths[i] + width_difference / (len(words) - 1)
+ i += 1
+ top += line_spacing
+ continue
+
+ # align left by anchor
+ if anchor[0] == "m":
+ left -= width_difference / 2.0
+ elif anchor[0] == "r":
+ left -= width_difference
+ parts.append(_Line(left, top, anchor, line))
+ top += line_spacing
+
+ return parts
+
+ def _get_bbox(
+ self,
+ text: str | bytes,
+ font: ImageFont.BaseImageFont | None = None,
+ anchor: str | None = None,
+ ) -> tuple[float, float, float, float]:
+ return (font or self.font).getbbox(
+ text,
+ self._get_fontmode(),
+ self.direction,
+ self.features,
+ self.language,
+ self.stroke_width,
+ anchor,
+ )
+
+ def get_bbox(
+ self,
+ xy: tuple[float, float] = (0, 0),
+ anchor: str | None = None,
+ align: str = "left",
+ ) -> tuple[float, float, float, float]:
+ """
+ Returns bounding box (in pixels) of text.
+
+ Use :py:meth:`get_length` to get the offset of following text with 1/64 pixel
+ precision. The bounding box includes extra margins for some fonts, e.g. italics
+ or accents.
+
+ :param xy: The anchor coordinates of the text.
+ :param anchor: The text anchor alignment. Determines the relative location of
+ the anchor to the text. The default alignment is top left,
+ specifically ``la`` for horizontal text and ``lt`` for
+ vertical text. See :ref:`text-anchors` for details.
+ :param align: For multiline text, ``"left"``, ``"center"``, ``"right"`` or
+ ``"justify"`` determines the relative alignment of lines. Use the
+ ``anchor`` parameter to specify the alignment to ``xy``.
+
+ :return: ``(left, top, right, bottom)`` bounding box
+ """
+ bbox: tuple[float, float, float, float] | None = None
+ for x, y, anchor, text in self._split(xy, anchor, align):
+ bbox_line = self._get_bbox(text, anchor=anchor)
+ bbox_line = (
+ bbox_line[0] + x,
+ bbox_line[1] + y,
+ bbox_line[2] + x,
+ bbox_line[3] + y,
+ )
+ if bbox is None:
+ bbox = bbox_line
+ else:
+ bbox = (
+ min(bbox[0], bbox_line[0]),
+ min(bbox[1], bbox_line[1]),
+ max(bbox[2], bbox_line[2]),
+ max(bbox[3], bbox_line[3]),
+ )
+
+ assert bbox is not None
+ return bbox
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageTk.py b/venv/lib/python3.11/site-packages/PIL/ImageTk.py
new file mode 100644
index 000000000..3a4cb81e9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageTk.py
@@ -0,0 +1,266 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# a Tk display interface
+#
+# History:
+# 96-04-08 fl Created
+# 96-09-06 fl Added getimage method
+# 96-11-01 fl Rewritten, removed image attribute and crop method
+# 97-05-09 fl Use PyImagingPaste method instead of image type
+# 97-05-12 fl Minor tweaks to match the IFUNC95 interface
+# 97-05-17 fl Support the "pilbitmap" booster patch
+# 97-06-05 fl Added file= and data= argument to image constructors
+# 98-03-09 fl Added width and height methods to Image classes
+# 98-07-02 fl Use default mode for "P" images without palette attribute
+# 98-07-02 fl Explicitly destroy Tkinter image objects
+# 99-07-24 fl Support multiple Tk interpreters (from Greg Couch)
+# 99-07-26 fl Automatically hook into Tkinter (if possible)
+# 99-08-15 fl Hook uses _imagingtk instead of _imaging
+#
+# Copyright (c) 1997-1999 by Secret Labs AB
+# Copyright (c) 1996-1997 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import tkinter
+from io import BytesIO
+from typing import Any
+
+from . import Image, ImageFile
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from ._typing import CapsuleType
+
+# --------------------------------------------------------------------
+# Check for Tkinter interface hooks
+
+
+def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
+ source = None
+ if "file" in kw:
+ source = kw.pop("file")
+ elif "data" in kw:
+ source = BytesIO(kw.pop("data"))
+ if not source:
+ return None
+ return Image.open(source)
+
+
+def _pyimagingtkcall(
+ command: str, photo: PhotoImage | tkinter.PhotoImage, ptr: CapsuleType
+) -> None:
+ tk = photo.tk
+ try:
+ tk.call(command, photo, repr(ptr))
+ except tkinter.TclError:
+ # activate Tkinter hook
+ # may raise an error if it cannot attach to Tkinter
+ from . import _imagingtk
+
+ _imagingtk.tkinit(tk.interpaddr())
+ tk.call(command, photo, repr(ptr))
+
+
+# --------------------------------------------------------------------
+# PhotoImage
+
+
+class PhotoImage:
+ """
+ A Tkinter-compatible photo image. This can be used
+ everywhere Tkinter expects an image object. If the image is an RGBA
+ image, pixels having alpha 0 are treated as transparent.
+
+ The constructor takes either a PIL image, or a mode and a size.
+ Alternatively, you can use the ``file`` or ``data`` options to initialize
+ the photo image object.
+
+ :param image: Either a PIL image, or a mode string. If a mode string is
+ used, a size must also be given.
+ :param size: If the first argument is a mode string, this defines the size
+ of the image.
+ :keyword file: A filename to load the image from (using
+ ``Image.open(file)``).
+ :keyword data: An 8-bit string containing image data (as loaded from an
+ image file).
+ """
+
+ def __init__(
+ self,
+ image: Image.Image | str | None = None,
+ size: tuple[int, int] | None = None,
+ **kw: Any,
+ ) -> None:
+ # Tk compatibility: file or data
+ if image is None:
+ image = _get_image_from_kw(kw)
+
+ if image is None:
+ msg = "Image is required"
+ raise ValueError(msg)
+ elif isinstance(image, str):
+ mode = image
+ image = None
+
+ if size is None:
+ msg = "If first argument is mode, size is required"
+ raise ValueError(msg)
+ else:
+ # got an image instead of a mode
+ mode = image.mode
+ if mode == "P":
+ # palette mapped data
+ image.apply_transparency()
+ image.load()
+ mode = image.palette.mode if image.palette else "RGB"
+ size = image.size
+ kw["width"], kw["height"] = size
+
+ if mode not in ["1", "L", "RGB", "RGBA"]:
+ mode = Image.getmodebase(mode)
+
+ self.__mode = mode
+ self.__size = size
+ self.__photo = tkinter.PhotoImage(**kw)
+ self.tk = self.__photo.tk
+ if image:
+ self.paste(image)
+
+ def __del__(self) -> None:
+ try:
+ name = self.__photo.name
+ except AttributeError:
+ return
+ self.__photo.name = None
+ try:
+ self.__photo.tk.call("image", "delete", name)
+ except Exception:
+ pass # ignore internal errors
+
+ def __str__(self) -> str:
+ """
+ Get the Tkinter photo image identifier. This method is automatically
+ called by Tkinter whenever a PhotoImage object is passed to a Tkinter
+ method.
+
+ :return: A Tkinter photo image identifier (a string).
+ """
+ return str(self.__photo)
+
+ def width(self) -> int:
+ """
+ Get the width of the image.
+
+ :return: The width, in pixels.
+ """
+ return self.__size[0]
+
+ def height(self) -> int:
+ """
+ Get the height of the image.
+
+ :return: The height, in pixels.
+ """
+ return self.__size[1]
+
+ def paste(self, im: Image.Image) -> None:
+ """
+ Paste a PIL image into the photo image. Note that this can
+ be very slow if the photo image is displayed.
+
+ :param im: A PIL image. The size must match the target region. If the
+ mode does not match, the image is converted to the mode of
+ the bitmap image.
+ """
+ # convert to blittable
+ ptr = im.getim()
+ image = im.im
+ if not image.isblock() or im.mode != self.__mode:
+ block = Image.core.new_block(self.__mode, im.size)
+ image.convert2(block, image) # convert directly between buffers
+ ptr = block.ptr
+
+ _pyimagingtkcall("PyImagingPhoto", self.__photo, ptr)
+
+
+# --------------------------------------------------------------------
+# BitmapImage
+
+
+class BitmapImage:
+ """
+ A Tkinter-compatible bitmap image. This can be used everywhere Tkinter
+ expects an image object.
+
+ The given image must have mode "1". Pixels having value 0 are treated as
+ transparent. Options, if any, are passed on to Tkinter. The most commonly
+ used option is ``foreground``, which is used to specify the color for the
+ non-transparent parts. See the Tkinter documentation for information on
+ how to specify colours.
+
+ :param image: A PIL image.
+ """
+
+ def __init__(self, image: Image.Image | None = None, **kw: Any) -> None:
+ # Tk compatibility: file or data
+ if image is None:
+ image = _get_image_from_kw(kw)
+
+ if image is None:
+ msg = "Image is required"
+ raise ValueError(msg)
+ self.__mode = image.mode
+ self.__size = image.size
+
+ self.__photo = tkinter.BitmapImage(data=image.tobitmap(), **kw)
+
+ def __del__(self) -> None:
+ try:
+ name = self.__photo.name
+ except AttributeError:
+ return
+ self.__photo.name = None
+ try:
+ self.__photo.tk.call("image", "delete", name)
+ except Exception:
+ pass # ignore internal errors
+
+ def width(self) -> int:
+ """
+ Get the width of the image.
+
+ :return: The width, in pixels.
+ """
+ return self.__size[0]
+
+ def height(self) -> int:
+ """
+ Get the height of the image.
+
+ :return: The height, in pixels.
+ """
+ return self.__size[1]
+
+ def __str__(self) -> str:
+ """
+ Get the Tkinter bitmap image identifier. This method is automatically
+ called by Tkinter whenever a BitmapImage object is passed to a Tkinter
+ method.
+
+ :return: A Tkinter bitmap image identifier (a string).
+ """
+ return str(self.__photo)
+
+
+def getimage(photo: PhotoImage) -> Image.Image:
+ """Copies the contents of a PhotoImage to a PIL image memory."""
+ im = Image.new("RGBA", (photo.width(), photo.height()))
+
+ _pyimagingtkcall("PyImagingPhotoGet", photo, im.getim())
+
+ return im
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageTransform.py b/venv/lib/python3.11/site-packages/PIL/ImageTransform.py
new file mode 100644
index 000000000..fb144ff38
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageTransform.py
@@ -0,0 +1,136 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# transform wrappers
+#
+# History:
+# 2002-04-08 fl Created
+#
+# Copyright (c) 2002 by Secret Labs AB
+# Copyright (c) 2002 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Any
+
+from . import Image
+
+
+class Transform(Image.ImageTransformHandler):
+ """Base class for other transforms defined in :py:mod:`~PIL.ImageTransform`."""
+
+ method: Image.Transform
+
+ def __init__(self, data: Sequence[Any]) -> None:
+ self.data = data
+
+ def getdata(self) -> tuple[Image.Transform, Sequence[int]]:
+ return self.method, self.data
+
+ def transform(
+ self,
+ size: tuple[int, int],
+ image: Image.Image,
+ **options: Any,
+ ) -> Image.Image:
+ """Perform the transform. Called from :py:meth:`.Image.transform`."""
+ # can be overridden
+ method, data = self.getdata()
+ return image.transform(size, method, data, **options)
+
+
+class AffineTransform(Transform):
+ """
+ Define an affine image transform.
+
+ This function takes a 6-tuple (a, b, c, d, e, f) which contain the first
+ two rows from the inverse of an affine transform matrix. For each pixel
+ (x, y) in the output image, the new value is taken from a position (a x +
+ b y + c, d x + e y + f) in the input image, rounded to nearest pixel.
+
+ This function can be used to scale, translate, rotate, and shear the
+ original image.
+
+ See :py:meth:`.Image.transform`
+
+ :param matrix: A 6-tuple (a, b, c, d, e, f) containing the first two rows
+ from the inverse of an affine transform matrix.
+ """
+
+ method = Image.Transform.AFFINE
+
+
+class PerspectiveTransform(Transform):
+ """
+ Define a perspective image transform.
+
+ This function takes an 8-tuple (a, b, c, d, e, f, g, h). For each pixel
+ (x, y) in the output image, the new value is taken from a position
+ ((a x + b y + c) / (g x + h y + 1), (d x + e y + f) / (g x + h y + 1)) in
+ the input image, rounded to nearest pixel.
+
+ This function can be used to scale, translate, rotate, and shear the
+ original image.
+
+ See :py:meth:`.Image.transform`
+
+ :param matrix: An 8-tuple (a, b, c, d, e, f, g, h).
+ """
+
+ method = Image.Transform.PERSPECTIVE
+
+
+class ExtentTransform(Transform):
+ """
+ Define a transform to extract a subregion from an image.
+
+ Maps a rectangle (defined by two corners) from the image to a rectangle of
+ the given size. The resulting image will contain data sampled from between
+ the corners, such that (x0, y0) in the input image will end up at (0,0) in
+ the output image, and (x1, y1) at size.
+
+ This method can be used to crop, stretch, shrink, or mirror an arbitrary
+ rectangle in the current image. It is slightly slower than crop, but about
+ as fast as a corresponding resize operation.
+
+ See :py:meth:`.Image.transform`
+
+ :param bbox: A 4-tuple (x0, y0, x1, y1) which specifies two points in the
+ input image's coordinate system. See :ref:`coordinate-system`.
+ """
+
+ method = Image.Transform.EXTENT
+
+
+class QuadTransform(Transform):
+ """
+ Define a quad image transform.
+
+ Maps a quadrilateral (a region defined by four corners) from the image to a
+ rectangle of the given size.
+
+ See :py:meth:`.Image.transform`
+
+ :param xy: An 8-tuple (x0, y0, x1, y1, x2, y2, x3, y3) which contain the
+ upper left, lower left, lower right, and upper right corner of the
+ source quadrilateral.
+ """
+
+ method = Image.Transform.QUAD
+
+
+class MeshTransform(Transform):
+ """
+ Define a mesh image transform. A mesh transform consists of one or more
+ individual quad transforms.
+
+ See :py:meth:`.Image.transform`
+
+ :param data: A list of (bbox, quad) tuples.
+ """
+
+ method = Image.Transform.MESH
diff --git a/venv/lib/python3.11/site-packages/PIL/ImageWin.py b/venv/lib/python3.11/site-packages/PIL/ImageWin.py
new file mode 100644
index 000000000..29263617d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImageWin.py
@@ -0,0 +1,247 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# a Windows DIB display interface
+#
+# History:
+# 1996-05-20 fl Created
+# 1996-09-20 fl Fixed subregion exposure
+# 1997-09-21 fl Added draw primitive (for tzPrint)
+# 2003-05-21 fl Added experimental Window/ImageWindow classes
+# 2003-09-05 fl Added fromstring/tostring methods
+#
+# Copyright (c) Secret Labs AB 1997-2003.
+# Copyright (c) Fredrik Lundh 1996-2003.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from . import Image
+
+
+class HDC:
+ """
+ Wraps an HDC integer. The resulting object can be passed to the
+ :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose`
+ methods.
+ """
+
+ def __init__(self, dc: int) -> None:
+ self.dc = dc
+
+ def __int__(self) -> int:
+ return self.dc
+
+
+class HWND:
+ """
+ Wraps an HWND integer. The resulting object can be passed to the
+ :py:meth:`~PIL.ImageWin.Dib.draw` and :py:meth:`~PIL.ImageWin.Dib.expose`
+ methods, instead of a DC.
+ """
+
+ def __init__(self, wnd: int) -> None:
+ self.wnd = wnd
+
+ def __int__(self) -> int:
+ return self.wnd
+
+
+class Dib:
+ """
+ A Windows bitmap with the given mode and size. The mode can be one of "1",
+ "L", "P", or "RGB".
+
+ If the display requires a palette, this constructor creates a suitable
+ palette and associates it with the image. For an "L" image, 128 graylevels
+ are allocated. For an "RGB" image, a 6x6x6 colour cube is used, together
+ with 20 graylevels.
+
+ To make sure that palettes work properly under Windows, you must call the
+ ``palette`` method upon certain events from Windows.
+
+ :param image: Either a PIL image, or a mode string. If a mode string is
+ used, a size must also be given. The mode can be one of "1",
+ "L", "P", or "RGB".
+ :param size: If the first argument is a mode string, this
+ defines the size of the image.
+ """
+
+ def __init__(
+ self, image: Image.Image | str, size: tuple[int, int] | None = None
+ ) -> None:
+ if isinstance(image, str):
+ mode = image
+ image = ""
+ if size is None:
+ msg = "If first argument is mode, size is required"
+ raise ValueError(msg)
+ else:
+ mode = image.mode
+ size = image.size
+ if mode not in ["1", "L", "P", "RGB"]:
+ mode = Image.getmodebase(mode)
+ self.image = Image.core.display(mode, size)
+ self.mode = mode
+ self.size = size
+ if image:
+ assert not isinstance(image, str)
+ self.paste(image)
+
+ def expose(self, handle: int | HDC | HWND) -> None:
+ """
+ Copy the bitmap contents to a device context.
+
+ :param handle: Device context (HDC), cast to a Python integer, or an
+ HDC or HWND instance. In PythonWin, you can use
+ ``CDC.GetHandleAttrib()`` to get a suitable handle.
+ """
+ handle_int = int(handle)
+ if isinstance(handle, HWND):
+ dc = self.image.getdc(handle_int)
+ try:
+ self.image.expose(dc)
+ finally:
+ self.image.releasedc(handle_int, dc)
+ else:
+ self.image.expose(handle_int)
+
+ def draw(
+ self,
+ handle: int | HDC | HWND,
+ dst: tuple[int, int, int, int],
+ src: tuple[int, int, int, int] | None = None,
+ ) -> None:
+ """
+ Same as expose, but allows you to specify where to draw the image, and
+ what part of it to draw.
+
+ The destination and source areas are given as 4-tuple rectangles. If
+ the source is omitted, the entire image is copied. If the source and
+ the destination have different sizes, the image is resized as
+ necessary.
+ """
+ if src is None:
+ src = (0, 0) + self.size
+ handle_int = int(handle)
+ if isinstance(handle, HWND):
+ dc = self.image.getdc(handle_int)
+ try:
+ self.image.draw(dc, dst, src)
+ finally:
+ self.image.releasedc(handle_int, dc)
+ else:
+ self.image.draw(handle_int, dst, src)
+
+ def query_palette(self, handle: int | HDC | HWND) -> int:
+ """
+ Installs the palette associated with the image in the given device
+ context.
+
+ This method should be called upon **QUERYNEWPALETTE** and
+ **PALETTECHANGED** events from Windows. If this method returns a
+ non-zero value, one or more display palette entries were changed, and
+ the image should be redrawn.
+
+ :param handle: Device context (HDC), cast to a Python integer, or an
+ HDC or HWND instance.
+ :return: The number of entries that were changed (if one or more entries,
+ this indicates that the image should be redrawn).
+ """
+ handle_int = int(handle)
+ if isinstance(handle, HWND):
+ handle = self.image.getdc(handle_int)
+ try:
+ result = self.image.query_palette(handle)
+ finally:
+ self.image.releasedc(handle, handle)
+ else:
+ result = self.image.query_palette(handle_int)
+ return result
+
+ def paste(
+ self, im: Image.Image, box: tuple[int, int, int, int] | None = None
+ ) -> None:
+ """
+ Paste a PIL image into the bitmap image.
+
+ :param im: A PIL image. The size must match the target region.
+ If the mode does not match, the image is converted to the
+ mode of the bitmap image.
+ :param box: A 4-tuple defining the left, upper, right, and
+ lower pixel coordinate. See :ref:`coordinate-system`. If
+ None is given instead of a tuple, all of the image is
+ assumed.
+ """
+ im.load()
+ if self.mode != im.mode:
+ im = im.convert(self.mode)
+ if box:
+ self.image.paste(im.im, box)
+ else:
+ self.image.paste(im.im)
+
+ def frombytes(self, buffer: bytes | memoryview) -> None:
+ """
+ Load display memory contents from byte data.
+
+ :param buffer: A buffer containing display data (usually
+ data returned from :py:func:`~PIL.ImageWin.Dib.tobytes`)
+ """
+ self.image.frombytes(buffer)
+
+ def tobytes(self) -> bytes:
+ """
+ Copy display memory contents to bytes object.
+
+ :return: A bytes object containing display data.
+ """
+ return self.image.tobytes()
+
+
+class Window:
+ """Create a Window with the given title size."""
+
+ def __init__(
+ self, title: str = "PIL", width: int | None = None, height: int | None = None
+ ) -> None:
+ self.hwnd = Image.core.createwindow(
+ title, self.__dispatcher, width or 0, height or 0
+ )
+
+ def __dispatcher(self, action: str, *args: int) -> None:
+ getattr(self, f"ui_handle_{action}")(*args)
+
+ def ui_handle_clear(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
+ pass
+
+ def ui_handle_damage(self, x0: int, y0: int, x1: int, y1: int) -> None:
+ pass
+
+ def ui_handle_destroy(self) -> None:
+ pass
+
+ def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
+ pass
+
+ def ui_handle_resize(self, width: int, height: int) -> None:
+ pass
+
+ def mainloop(self) -> None:
+ Image.core.eventloop()
+
+
+class ImageWindow(Window):
+ """Create an image window which displays the given image."""
+
+ def __init__(self, image: Image.Image | Dib, title: str = "PIL") -> None:
+ if not isinstance(image, Dib):
+ image = Dib(image)
+ self.image = image
+ width, height = image.size
+ super().__init__(title, width=width, height=height)
+
+ def ui_handle_repair(self, dc: int, x0: int, y0: int, x1: int, y1: int) -> None:
+ self.image.draw(dc, (x0, y0, x1, y1))
diff --git a/venv/lib/python3.11/site-packages/PIL/ImtImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/ImtImagePlugin.py
new file mode 100644
index 000000000..c4eccee34
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/ImtImagePlugin.py
@@ -0,0 +1,103 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# IM Tools support for PIL
+#
+# history:
+# 1996-05-27 fl Created (read 8-bit images only)
+# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.2)
+#
+# Copyright (c) Secret Labs AB 1997-2001.
+# Copyright (c) Fredrik Lundh 1996-2001.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import re
+
+from . import Image, ImageFile
+
+#
+# --------------------------------------------------------------------
+
+field = re.compile(rb"([a-z]*) ([^ \r\n]*)")
+
+
+##
+# Image plugin for IM Tools images.
+
+
+class ImtImageFile(ImageFile.ImageFile):
+ format = "IMT"
+ format_description = "IM Tools"
+
+ def _open(self) -> None:
+ # Quick rejection: if there's not a LF among the first
+ # 100 bytes, this is (probably) not a text header.
+
+ assert self.fp is not None
+
+ buffer = self.fp.read(100)
+ if b"\n" not in buffer:
+ msg = "not an IM file"
+ raise SyntaxError(msg)
+
+ xsize = ysize = 0
+
+ while True:
+ if buffer:
+ s = buffer[:1]
+ buffer = buffer[1:]
+ else:
+ s = self.fp.read(1)
+ if not s:
+ break
+
+ if s == b"\x0c":
+ # image data begins
+ self.tile = [
+ ImageFile._Tile(
+ "raw",
+ (0, 0) + self.size,
+ self.fp.tell() - len(buffer),
+ self.mode,
+ )
+ ]
+
+ break
+
+ else:
+ # read key/value pair
+ if b"\n" not in buffer:
+ buffer += self.fp.read(100)
+ lines = buffer.split(b"\n")
+ s += lines.pop(0)
+ buffer = b"\n".join(lines)
+ if len(s) == 1 or len(s) > 100:
+ break
+ if s[0] == ord(b"*"):
+ continue # comment
+
+ m = field.match(s)
+ if not m:
+ break
+ k, v = m.group(1, 2)
+ if k == b"width":
+ xsize = int(v)
+ self._size = xsize, ysize
+ elif k == b"height":
+ ysize = int(v)
+ self._size = xsize, ysize
+ elif k == b"pixel" and v == b"n8":
+ self._mode = "L"
+
+
+#
+# --------------------------------------------------------------------
+
+Image.register_open(ImtImageFile.format, ImtImageFile)
+
+#
+# no extension registered (".im" is simply too common)
diff --git a/venv/lib/python3.11/site-packages/PIL/IptcImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/IptcImagePlugin.py
new file mode 100644
index 000000000..9c8be8b4e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/IptcImagePlugin.py
@@ -0,0 +1,226 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# IPTC/NAA file handling
+#
+# history:
+# 1995-10-01 fl Created
+# 1998-03-09 fl Cleaned up and added to PIL
+# 2002-06-18 fl Added getiptcinfo helper
+#
+# Copyright (c) Secret Labs AB 1997-2002.
+# Copyright (c) Fredrik Lundh 1995.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from io import BytesIO
+from typing import cast
+
+from . import Image, ImageFile
+from ._binary import i16be as i16
+from ._binary import i32be as i32
+
+COMPRESSION = {1: "raw", 5: "jpeg"}
+
+
+#
+# Helpers
+
+
+def _i(c: bytes) -> int:
+ return i32((b"\0\0\0\0" + c)[-4:])
+
+
+##
+# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
+# from TIFF and JPEG files, use the getiptcinfo function.
+
+
+class IptcImageFile(ImageFile.ImageFile):
+ format = "IPTC"
+ format_description = "IPTC/NAA"
+
+ def getint(self, key: tuple[int, int]) -> int:
+ return _i(self.info[key])
+
+ def field(self) -> tuple[tuple[int, int] | None, int]:
+ #
+ # get a IPTC field header
+ assert self.fp is not None
+ s = self.fp.read(5)
+ if not s.strip(b"\x00"):
+ return None, 0
+
+ tag = s[1], s[2]
+
+ # syntax
+ if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]:
+ msg = "invalid IPTC/NAA file"
+ raise SyntaxError(msg)
+
+ # field size
+ size = s[3]
+ if size > 132:
+ msg = "illegal field length in IPTC/NAA file"
+ raise OSError(msg)
+ elif size == 128:
+ size = 0
+ elif size > 128:
+ size = _i(self.fp.read(size - 128))
+ else:
+ size = i16(s, 3)
+
+ return tag, size
+
+ def _open(self) -> None:
+ # load descriptive fields
+ assert self.fp is not None
+ while True:
+ offset = self.fp.tell()
+ tag, size = self.field()
+ if not tag or tag == (8, 10):
+ break
+ if size:
+ tagdata = self.fp.read(size)
+ else:
+ tagdata = None
+ if tag in self.info:
+ if isinstance(self.info[tag], list):
+ self.info[tag].append(tagdata)
+ else:
+ self.info[tag] = [self.info[tag], tagdata]
+ else:
+ self.info[tag] = tagdata
+
+ # mode
+ layers = self.info[(3, 60)][0]
+ component = self.info[(3, 60)][1]
+ if layers == 1 and not component:
+ self._mode = "L"
+ band = None
+ else:
+ if layers == 3 and component:
+ self._mode = "RGB"
+ elif layers == 4 and component:
+ self._mode = "CMYK"
+ if (3, 65) in self.info:
+ band = self.info[(3, 65)][0] - 1
+ else:
+ band = 0
+
+ # size
+ self._size = self.getint((3, 20)), self.getint((3, 30))
+
+ # compression
+ try:
+ compression = COMPRESSION[self.getint((3, 120))]
+ except KeyError as e:
+ msg = "Unknown IPTC image compression"
+ raise OSError(msg) from e
+
+ # tile
+ if tag == (8, 10):
+ self.tile = [
+ ImageFile._Tile("iptc", (0, 0) + self.size, offset, (compression, band))
+ ]
+
+ def load(self) -> Image.core.PixelAccess | None:
+ if self.tile:
+ args = self.tile[0].args
+ assert isinstance(args, tuple)
+ compression, band = args
+
+ assert self.fp is not None
+ self.fp.seek(self.tile[0].offset)
+
+ # Copy image data to temporary file
+ o = BytesIO()
+ if compression == "raw":
+ # To simplify access to the extracted file,
+ # prepend a PPM header
+ o.write(b"P5\n%d %d\n255\n" % self.size)
+ while True:
+ type, size = self.field()
+ if type != (8, 10):
+ break
+ while size > 0:
+ s = self.fp.read(min(size, 8192))
+ if not s:
+ break
+ o.write(s)
+ size -= len(s)
+
+ with Image.open(o) as _im:
+ if band is not None:
+ bands = [Image.new("L", _im.size)] * Image.getmodebands(self.mode)
+ bands[band] = _im
+ im = Image.merge(self.mode, bands)
+ else:
+ im = _im
+ im.load()
+ self.im = im.im
+ self.tile = []
+ return ImageFile.ImageFile.load(self)
+
+
+Image.register_open(IptcImageFile.format, IptcImageFile)
+
+Image.register_extension(IptcImageFile.format, ".iim")
+
+
+def getiptcinfo(
+ im: ImageFile.ImageFile,
+) -> dict[tuple[int, int], bytes | list[bytes]] | None:
+ """
+ Get IPTC information from TIFF, JPEG, or IPTC file.
+
+ :param im: An image containing IPTC data.
+ :returns: A dictionary containing IPTC information, or None if
+ no IPTC information block was found.
+ """
+ from . import JpegImagePlugin, TiffImagePlugin
+
+ data = None
+
+ if isinstance(im, IptcImageFile):
+ # return info dictionary right away
+ return {k: v for k, v in im.info.items() if isinstance(k, tuple)}
+
+ elif isinstance(im, JpegImagePlugin.JpegImageFile):
+ # extract the IPTC/NAA resource
+ photoshop = im.info.get("photoshop")
+ if photoshop:
+ data = photoshop.get(0x0404)
+
+ elif isinstance(im, TiffImagePlugin.TiffImageFile):
+ # get raw data from the IPTC/NAA tag (PhotoShop tags the data
+ # as 4-byte integers, so we cannot use the get method...)
+ try:
+ data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK]
+ except KeyError:
+ pass
+
+ if data is None:
+ return None # no properties
+
+ # create an IptcImagePlugin object without initializing it
+ class FakeImage:
+ pass
+
+ fake_im = FakeImage()
+ fake_im.__class__ = IptcImageFile # type: ignore[assignment]
+ iptc_im = cast(IptcImageFile, fake_im)
+
+ # parse the IPTC information chunk
+ iptc_im.info = {}
+ iptc_im.fp = BytesIO(data)
+
+ try:
+ iptc_im._open()
+ except (IndexError, KeyError):
+ pass # expected failure
+
+ return {k: v for k, v in iptc_im.info.items() if isinstance(k, tuple)}
diff --git a/venv/lib/python3.11/site-packages/PIL/Jpeg2KImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/Jpeg2KImagePlugin.py
new file mode 100644
index 000000000..ca982f06a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/Jpeg2KImagePlugin.py
@@ -0,0 +1,466 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# JPEG2000 file handling
+#
+# History:
+# 2014-03-12 ajh Created
+# 2021-06-30 rogermb Extract dpi information from the 'resc' header box
+#
+# Copyright (c) 2014 Coriolis Systems Limited
+# Copyright (c) 2014 Alastair Houghton
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+import os
+import struct
+from typing import cast
+
+from . import Image, ImageFile, ImagePalette, _binary
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import IO
+
+
+class BoxReader:
+ """
+ A small helper class to read fields stored in JPEG2000 header boxes
+ and to easily step into and read sub-boxes.
+ """
+
+ def __init__(self, fp: IO[bytes], length: int = -1) -> None:
+ self.fp = fp
+ self.has_length = length >= 0
+ self.length = length
+ self.remaining_in_box = -1
+
+ def _can_read(self, num_bytes: int) -> bool:
+ if self.has_length and self.fp.tell() + num_bytes > self.length:
+ # Outside box: ensure we don't read past the known file length
+ return False
+ if self.remaining_in_box >= 0:
+ # Inside box contents: ensure read does not go past box boundaries
+ return num_bytes <= self.remaining_in_box
+ else:
+ return True # No length known, just read
+
+ def _read_bytes(self, num_bytes: int) -> bytes:
+ if not self._can_read(num_bytes):
+ msg = "Not enough data in header"
+ raise SyntaxError(msg)
+
+ data = self.fp.read(num_bytes)
+ if len(data) < num_bytes:
+ msg = f"Expected to read {num_bytes} bytes but only got {len(data)}."
+ raise OSError(msg)
+
+ if self.remaining_in_box > 0:
+ self.remaining_in_box -= num_bytes
+ return data
+
+ def read_fields(self, field_format: str) -> tuple[int | bytes, ...]:
+ size = struct.calcsize(field_format)
+ data = self._read_bytes(size)
+ return struct.unpack(field_format, data)
+
+ def read_boxes(self) -> BoxReader:
+ size = self.remaining_in_box
+ data = self._read_bytes(size)
+ return BoxReader(io.BytesIO(data), size)
+
+ def has_next_box(self) -> bool:
+ if self.has_length:
+ return self.fp.tell() + self.remaining_in_box < self.length
+ else:
+ return True
+
+ def next_box_type(self) -> bytes:
+ # Skip the rest of the box if it has not been read
+ if self.remaining_in_box > 0:
+ self.fp.seek(self.remaining_in_box, os.SEEK_CUR)
+ self.remaining_in_box = -1
+
+ # Read the length and type of the next box
+ lbox, tbox = cast(tuple[int, bytes], self.read_fields(">I4s"))
+ if lbox == 1:
+ lbox = cast(int, self.read_fields(">Q")[0])
+ hlen = 16
+ else:
+ hlen = 8
+
+ if lbox < hlen or not self._can_read(lbox - hlen):
+ msg = "Invalid header length"
+ raise SyntaxError(msg)
+
+ self.remaining_in_box = lbox - hlen
+ return tbox
+
+
+def _parse_codestream(fp: IO[bytes]) -> tuple[tuple[int, int], str]:
+ """Parse the JPEG 2000 codestream to extract the size and component
+ count from the SIZ marker segment, returning a PIL (size, mode) tuple."""
+
+ hdr = fp.read(2)
+ lsiz = _binary.i16be(hdr)
+ if lsiz < 38:
+ msg = "SIZ marker length must be at least 38"
+ raise ValueError(msg)
+ siz = hdr + fp.read(lsiz - 2)
+ lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from(
+ ">HHIIIIIIIIH", siz
+ )
+
+ size = (xsiz - xosiz, ysiz - yosiz)
+ if csiz == 1:
+ ssiz = struct.unpack_from(">B", siz, 38)
+ if (ssiz[0] & 0x7F) + 1 > 8:
+ mode = "I;16"
+ else:
+ mode = "L"
+ elif csiz == 2:
+ mode = "LA"
+ elif csiz == 3:
+ mode = "RGB"
+ elif csiz == 4:
+ mode = "RGBA"
+ else:
+ msg = "unable to determine J2K image mode"
+ raise SyntaxError(msg)
+
+ return size, mode
+
+
+def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
+ """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
+ calculated as (num / denom) * 10^exp and stored in dots per meter,
+ to floating-point dots per inch."""
+ if denom == 0:
+ return None
+ return (254 * num * (10**exp)) / (10000 * denom)
+
+
+def _parse_jp2_header(
+ fp: IO[bytes],
+) -> tuple[
+ tuple[int, int],
+ str,
+ str | None,
+ tuple[float, float] | None,
+ ImagePalette.ImagePalette | None,
+]:
+ """Parse the JP2 header box to extract size, component count,
+ color space information, and optionally DPI information,
+ returning a (size, mode, mimetype, dpi) tuple."""
+
+ # Find the JP2 header box
+ reader = BoxReader(fp)
+ header = None
+ mimetype = None
+ while reader.has_next_box():
+ tbox = reader.next_box_type()
+
+ if tbox == b"jp2h":
+ header = reader.read_boxes()
+ break
+ elif tbox == b"ftyp":
+ if reader.read_fields(">4s")[0] == b"jpx ":
+ mimetype = "image/jpx"
+ assert header is not None
+
+ size = None
+ mode = None
+ bpc = None
+ nc = None
+ dpi = None # 2-tuple of DPI info, or None
+ palette = None
+ colr = None
+
+ while header.has_next_box():
+ tbox = header.next_box_type()
+
+ if tbox == b"ihdr":
+ height, width, nc, bpc = header.read_fields(">IIHB")
+ assert isinstance(height, int)
+ assert isinstance(width, int)
+ assert isinstance(bpc, int)
+ size = (width, height)
+ if nc == 1 and (bpc & 0x7F) > 8:
+ mode = "I;16"
+ elif nc == 1:
+ mode = "L"
+ elif nc == 2:
+ mode = "LA"
+ elif nc == 3:
+ mode = "RGB"
+ elif nc == 4:
+ mode = "RGBA"
+ elif tbox == b"colr":
+ meth, _, _, enumcs = header.read_fields(">BBBI")
+ if meth == 1:
+ if enumcs in (0, 15):
+ colr = "1"
+ elif enumcs == 12:
+ colr = "CMYK"
+ if nc == 4:
+ mode = "CMYK"
+ elif enumcs == 17:
+ colr = "L"
+ elif tbox == b"pclr" and mode in ("L", "LA") and colr not in ("1", "L"):
+ ne, npc = header.read_fields(">HB")
+ assert isinstance(ne, int)
+ assert isinstance(npc, int)
+ max_bitdepth = 0
+ for bitdepth in header.read_fields(">" + ("B" * npc)):
+ assert isinstance(bitdepth, int)
+ if bitdepth > max_bitdepth:
+ max_bitdepth = bitdepth
+ if max_bitdepth <= 8:
+ if npc == 4:
+ palette_mode = "CMYK" if colr == "CMYK" else "RGBA"
+ else:
+ palette_mode = "RGB"
+ palette = ImagePalette.ImagePalette(palette_mode)
+ for i in range(ne):
+ color: list[int] = []
+ for value in header.read_fields(">" + ("B" * npc)):
+ assert isinstance(value, int)
+ color.append(value)
+ palette.getcolor(tuple(color))
+ mode = "P" if mode == "L" else "PA"
+ elif tbox == b"res ":
+ res = header.read_boxes()
+ while res.has_next_box():
+ tres = res.next_box_type()
+ if tres == b"resc":
+ vrcn, vrcd, hrcn, hrcd, vrce, hrce = res.read_fields(">HHHHBB")
+ assert isinstance(vrcn, int)
+ assert isinstance(vrcd, int)
+ assert isinstance(hrcn, int)
+ assert isinstance(hrcd, int)
+ assert isinstance(vrce, int)
+ assert isinstance(hrce, int)
+ hres = _res_to_dpi(hrcn, hrcd, hrce)
+ vres = _res_to_dpi(vrcn, vrcd, vrce)
+ if hres is not None and vres is not None:
+ dpi = (hres, vres)
+ break
+
+ if size is None or mode is None:
+ msg = "Malformed JP2 header"
+ raise SyntaxError(msg)
+
+ return size, mode, mimetype, dpi, palette
+
+
+##
+# Image plugin for JPEG2000 images.
+
+
+class Jpeg2KImageFile(ImageFile.ImageFile):
+ format = "JPEG2000"
+ format_description = "JPEG 2000 (ISO 15444)"
+
+ def _open(self) -> None:
+ assert self.fp is not None
+ sig = self.fp.read(4)
+ if sig == b"\xff\x4f\xff\x51":
+ self.codec = "j2k"
+ self._size, self._mode = _parse_codestream(self.fp)
+ self._parse_comment()
+ else:
+ sig = sig + self.fp.read(8)
+
+ if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a":
+ self.codec = "jp2"
+ header = _parse_jp2_header(self.fp)
+ self._size, self._mode, self.custom_mimetype, dpi, self.palette = header
+ if dpi is not None:
+ self.info["dpi"] = dpi
+ if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"):
+ hdr = self.fp.read(2)
+ length = _binary.i16be(hdr)
+ self.fp.seek(length - 2, os.SEEK_CUR)
+ self._parse_comment()
+ else:
+ msg = "not a JPEG 2000 file"
+ raise SyntaxError(msg)
+
+ self._reduce = 0
+ self.layers = 0
+
+ fd = -1
+ length = -1
+
+ try:
+ fd = self.fp.fileno()
+ length = os.fstat(fd).st_size
+ except Exception:
+ fd = -1
+ try:
+ pos = self.fp.tell()
+ self.fp.seek(0, io.SEEK_END)
+ length = self.fp.tell()
+ self.fp.seek(pos)
+ except Exception:
+ length = -1
+
+ self.tile = [
+ ImageFile._Tile(
+ "jpeg2k",
+ (0, 0) + self.size,
+ 0,
+ (self.codec, self._reduce, self.layers, fd, length),
+ )
+ ]
+
+ def _parse_comment(self) -> None:
+ assert self.fp is not None
+ while True:
+ marker = self.fp.read(2)
+ if not marker:
+ break
+ typ = marker[1]
+ if typ in (0x90, 0xD9):
+ # Start of tile or end of codestream
+ break
+ hdr = self.fp.read(2)
+ length = _binary.i16be(hdr)
+ if length < 2:
+ msg = "Marker length too small"
+ raise ValueError(msg)
+ if typ == 0x64:
+ # Comment
+ self.info["comment"] = self.fp.read(length - 2)[2:]
+ break
+ else:
+ self.fp.seek(length - 2, os.SEEK_CUR)
+
+ @property # type: ignore[override]
+ def reduce(
+ self,
+ ) -> (
+ Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image]
+ | int
+ ):
+ # https://github.com/python-pillow/Pillow/issues/4343 found that the
+ # new Image 'reduce' method was shadowed by this plugin's 'reduce'
+ # property. This attempts to allow for both scenarios
+ return self._reduce or super().reduce
+
+ @reduce.setter
+ def reduce(self, value: int) -> None:
+ self._reduce = value
+
+ def load(self) -> Image.core.PixelAccess | None:
+ if self.tile and self._reduce:
+ power = 1 << self._reduce
+ adjust = power >> 1
+ self._size = (
+ int((self.size[0] + adjust) / power),
+ int((self.size[1] + adjust) / power),
+ )
+
+ # Update the reduce and layers settings
+ t = self.tile[0]
+ assert isinstance(t[3], tuple)
+ t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
+ self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)]
+
+ return ImageFile.ImageFile.load(self)
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(
+ (b"\xff\x4f\xff\x51", b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a")
+ )
+
+
+# ------------------------------------------------------------
+# Save support
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ # Get the keyword arguments
+ info = im.encoderinfo
+
+ if isinstance(filename, str):
+ filename = filename.encode()
+ if filename.endswith(b".j2k") or info.get("no_jp2", False):
+ kind = "j2k"
+ else:
+ kind = "jp2"
+
+ offset = info.get("offset", None)
+ tile_offset = info.get("tile_offset", None)
+ tile_size = info.get("tile_size", None)
+ quality_mode = info.get("quality_mode", "rates")
+ quality_layers = info.get("quality_layers", None)
+ if quality_layers is not None and not (
+ isinstance(quality_layers, (list, tuple))
+ and all(
+ isinstance(quality_layer, (int, float)) for quality_layer in quality_layers
+ )
+ ):
+ msg = "quality_layers must be a sequence of numbers"
+ raise ValueError(msg)
+
+ num_resolutions = info.get("num_resolutions", 0)
+ cblk_size = info.get("codeblock_size", None)
+ precinct_size = info.get("precinct_size", None)
+ irreversible = info.get("irreversible", False)
+ progression = info.get("progression", "LRCP")
+ cinema_mode = info.get("cinema_mode", "no")
+ mct = info.get("mct", 0)
+ signed = info.get("signed", False)
+ comment = info.get("comment")
+ if isinstance(comment, str):
+ comment = comment.encode()
+ plt = info.get("plt", False)
+
+ fd = -1
+ if hasattr(fp, "fileno"):
+ try:
+ fd = fp.fileno()
+ except Exception:
+ fd = -1
+
+ im.encoderconfig = (
+ offset,
+ tile_offset,
+ tile_size,
+ quality_mode,
+ quality_layers,
+ num_resolutions,
+ cblk_size,
+ precinct_size,
+ irreversible,
+ progression,
+ cinema_mode,
+ mct,
+ signed,
+ fd,
+ comment,
+ plt,
+ )
+
+ ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)])
+
+
+# ------------------------------------------------------------
+# Registry stuff
+
+
+Image.register_open(Jpeg2KImageFile.format, Jpeg2KImageFile, _accept)
+Image.register_save(Jpeg2KImageFile.format, _save)
+
+Image.register_extensions(
+ Jpeg2KImageFile.format, [".jp2", ".j2k", ".jpc", ".jpf", ".jpx", ".j2c"]
+)
+
+Image.register_mime(Jpeg2KImageFile.format, "image/jp2")
diff --git a/venv/lib/python3.11/site-packages/PIL/JpegImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/JpegImagePlugin.py
new file mode 100644
index 000000000..46320eb3b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/JpegImagePlugin.py
@@ -0,0 +1,889 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# JPEG (JFIF) file handling
+#
+# See "Digital Compression and Coding of Continuous-Tone Still Images,
+# Part 1, Requirements and Guidelines" (CCITT T.81 / ISO 10918-1)
+#
+# History:
+# 1995-09-09 fl Created
+# 1995-09-13 fl Added full parser
+# 1996-03-25 fl Added hack to use the IJG command line utilities
+# 1996-05-05 fl Workaround Photoshop 2.5 CMYK polarity bug
+# 1996-05-28 fl Added draft support, JFIF version (0.1)
+# 1996-12-30 fl Added encoder options, added progression property (0.2)
+# 1997-08-27 fl Save mode 1 images as BW (0.3)
+# 1998-07-12 fl Added YCbCr to draft and save methods (0.4)
+# 1998-10-19 fl Don't hang on files using 16-bit DQT's (0.4.1)
+# 2001-04-16 fl Extract DPI settings from JFIF files (0.4.2)
+# 2002-07-01 fl Skip pad bytes before markers; identify Exif files (0.4.3)
+# 2003-04-25 fl Added experimental EXIF decoder (0.5)
+# 2003-06-06 fl Added experimental EXIF GPSinfo decoder
+# 2003-09-13 fl Extract COM markers
+# 2009-09-06 fl Added icc_profile support (from Florian Hoech)
+# 2009-03-06 fl Changed CMYK handling; always use Adobe polarity (0.6)
+# 2009-03-08 fl Added subsampling support (from Justin Huff).
+#
+# Copyright (c) 1997-2003 by Secret Labs AB.
+# Copyright (c) 1995-1996 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import array
+import io
+import math
+import os
+import struct
+import subprocess
+import sys
+import tempfile
+import warnings
+
+from . import Image, ImageFile
+from ._binary import i16be as i16
+from ._binary import i32be as i32
+from ._binary import o8
+from ._binary import o16be as o16
+from .JpegPresets import presets
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import IO, Any
+
+ from .MpoImagePlugin import MpoImageFile
+
+#
+# Parser
+
+
+def Skip(self: JpegImageFile, marker: int) -> None:
+ assert self.fp is not None
+ n = i16(self.fp.read(2)) - 2
+ ImageFile._safe_read(self.fp, n)
+
+
+def APP(self: JpegImageFile, marker: int) -> None:
+ #
+ # Application marker. Store these in the APP dictionary.
+ # Also look for well-known application markers.
+
+ assert self.fp is not None
+ n = i16(self.fp.read(2)) - 2
+ s = ImageFile._safe_read(self.fp, n)
+
+ app = f"APP{marker & 15}"
+
+ self.app[app] = s # compatibility
+ self.applist.append((app, s))
+
+ if marker == 0xFFE0 and s.startswith(b"JFIF"):
+ # extract JFIF information
+ self.info["jfif"] = version = i16(s, 5) # version
+ self.info["jfif_version"] = divmod(version, 256)
+ # extract JFIF properties
+ try:
+ jfif_unit = s[7]
+ jfif_density = i16(s, 8), i16(s, 10)
+ except Exception:
+ pass
+ else:
+ if jfif_unit == 1:
+ self.info["dpi"] = jfif_density
+ elif jfif_unit == 2: # cm
+ # 1 dpcm = 2.54 dpi
+ self.info["dpi"] = tuple(d * 2.54 for d in jfif_density)
+ self.info["jfif_unit"] = jfif_unit
+ self.info["jfif_density"] = jfif_density
+ elif marker == 0xFFE1 and s.startswith(b"Exif\0\0"):
+ # extract EXIF information
+ if "exif" in self.info:
+ self.info["exif"] += s[6:]
+ else:
+ self.info["exif"] = s
+ self._exif_offset = self.fp.tell() - n + 6
+ elif marker == 0xFFE1 and s.startswith(b"http://ns.adobe.com/xap/1.0/\x00"):
+ self.info["xmp"] = s.split(b"\x00", 1)[1]
+ elif marker == 0xFFE2 and s.startswith(b"FPXR\0"):
+ # extract FlashPix information (incomplete)
+ self.info["flashpix"] = s # FIXME: value will change
+ elif marker == 0xFFE2 and s.startswith(b"ICC_PROFILE\0"):
+ # Since an ICC profile can be larger than the maximum size of
+ # a JPEG marker (64K), we need provisions to split it into
+ # multiple markers. The format defined by the ICC specifies
+ # one or more APP2 markers containing the following data:
+ # Identifying string ASCII "ICC_PROFILE\0" (12 bytes)
+ # Marker sequence number 1, 2, etc (1 byte)
+ # Number of markers Total of APP2's used (1 byte)
+ # Profile data (remainder of APP2 data)
+ # Decoders should use the marker sequence numbers to
+ # reassemble the profile, rather than assuming that the APP2
+ # markers appear in the correct sequence.
+ self.icclist.append(s)
+ elif marker == 0xFFED and s.startswith(b"Photoshop 3.0\x00"):
+ # parse the image resource block
+ offset = 14
+ photoshop = self.info.setdefault("photoshop", {})
+ try:
+ while s[offset : offset + 4] == b"8BIM":
+ offset += 4
+ # resource code
+ code = i16(s, offset)
+ offset += 2
+ # resource name (usually empty)
+ name_len = s[offset]
+ # name = s[offset+1:offset+1+name_len]
+ offset += 1 + name_len
+ offset += offset & 1 # align
+ # resource data block
+ size = i32(s, offset)
+ offset += 4
+ data = s[offset : offset + size]
+ if code == 0x03ED: # ResolutionInfo
+ photoshop[code] = {
+ "XResolution": i32(data, 0) / 65536,
+ "DisplayedUnitsX": i16(data, 4),
+ "YResolution": i32(data, 8) / 65536,
+ "DisplayedUnitsY": i16(data, 12),
+ }
+ else:
+ photoshop[code] = data
+ offset += size
+ offset += offset & 1 # align
+ except struct.error:
+ pass # insufficient data
+
+ elif marker == 0xFFEE and s.startswith(b"Adobe"):
+ self.info["adobe"] = i16(s, 5)
+ # extract Adobe custom properties
+ try:
+ adobe_transform = s[11]
+ except IndexError:
+ pass
+ else:
+ self.info["adobe_transform"] = adobe_transform
+ elif marker == 0xFFE2 and s.startswith(b"MPF\0"):
+ # extract MPO information
+ self.info["mp"] = s[4:]
+ # offset is current location minus buffer size
+ # plus constant header size
+ self.info["mpoffset"] = self.fp.tell() - n + 4
+
+
+def COM(self: JpegImageFile, marker: int) -> None:
+ #
+ # Comment marker. Store these in the APP dictionary.
+ assert self.fp is not None
+ n = i16(self.fp.read(2)) - 2
+ s = ImageFile._safe_read(self.fp, n)
+
+ self.info["comment"] = s
+ self.app["COM"] = s # compatibility
+ self.applist.append(("COM", s))
+
+
+def SOF(self: JpegImageFile, marker: int) -> None:
+ #
+ # Start of frame marker. Defines the size and mode of the
+ # image. JPEG is colour blind, so we use some simple
+ # heuristics to map the number of layers to an appropriate
+ # mode. Note that this could be made a bit brighter, by
+ # looking for JFIF and Adobe APP markers.
+
+ assert self.fp is not None
+ n = i16(self.fp.read(2)) - 2
+ s = ImageFile._safe_read(self.fp, n)
+ self._size = i16(s, 3), i16(s, 1)
+ if self._im is not None and self.size != self.im.size:
+ self._im = None
+
+ self.bits = s[0]
+ if self.bits != 8:
+ msg = f"cannot handle {self.bits}-bit layers"
+ raise SyntaxError(msg)
+
+ self.layers = s[5]
+ if self.layers == 1:
+ self._mode = "L"
+ elif self.layers == 3:
+ self._mode = "RGB"
+ elif self.layers == 4:
+ self._mode = "CMYK"
+ else:
+ msg = f"cannot handle {self.layers}-layer images"
+ raise SyntaxError(msg)
+
+ if marker in [0xFFC2, 0xFFC6, 0xFFCA, 0xFFCE]:
+ self.info["progressive"] = self.info["progression"] = 1
+
+ if self.icclist:
+ # fixup icc profile
+ self.icclist.sort() # sort by sequence number
+ if self.icclist[0][13] == len(self.icclist):
+ profile = [p[14:] for p in self.icclist]
+ icc_profile = b"".join(profile)
+ else:
+ icc_profile = None # wrong number of fragments
+ self.info["icc_profile"] = icc_profile
+ self.icclist = []
+
+ for i in range(6, len(s), 3):
+ t = s[i : i + 3]
+ # 4-tuples: id, vsamp, hsamp, qtable
+ self.layer.append((t[0], t[1] // 16, t[1] & 15, t[2]))
+
+
+def DQT(self: JpegImageFile, marker: int) -> None:
+ #
+ # Define quantization table. Note that there might be more
+ # than one table in each marker.
+
+ # FIXME: The quantization tables can be used to estimate the
+ # compression quality.
+
+ assert self.fp is not None
+ n = i16(self.fp.read(2)) - 2
+ s = ImageFile._safe_read(self.fp, n)
+ while len(s):
+ v = s[0]
+ precision = 1 if (v // 16 == 0) else 2 # in bytes
+ qt_length = 1 + precision * 64
+ if len(s) < qt_length:
+ msg = "bad quantization table marker"
+ raise SyntaxError(msg)
+ data = array.array("B" if precision == 1 else "H", s[1:qt_length])
+ if sys.byteorder == "little" and precision > 1:
+ data.byteswap() # the values are always big-endian
+ self.quantization[v & 15] = [data[i] for i in zigzag_index]
+ s = s[qt_length:]
+
+
+#
+# JPEG marker table
+
+MARKER = {
+ 0xFFC0: ("SOF0", "Baseline DCT", SOF),
+ 0xFFC1: ("SOF1", "Extended Sequential DCT", SOF),
+ 0xFFC2: ("SOF2", "Progressive DCT", SOF),
+ 0xFFC3: ("SOF3", "Spatial lossless", SOF),
+ 0xFFC4: ("DHT", "Define Huffman table", Skip),
+ 0xFFC5: ("SOF5", "Differential sequential DCT", SOF),
+ 0xFFC6: ("SOF6", "Differential progressive DCT", SOF),
+ 0xFFC7: ("SOF7", "Differential spatial", SOF),
+ 0xFFC8: ("JPG", "Extension", None),
+ 0xFFC9: ("SOF9", "Extended sequential DCT (AC)", SOF),
+ 0xFFCA: ("SOF10", "Progressive DCT (AC)", SOF),
+ 0xFFCB: ("SOF11", "Spatial lossless DCT (AC)", SOF),
+ 0xFFCC: ("DAC", "Define arithmetic coding conditioning", Skip),
+ 0xFFCD: ("SOF13", "Differential sequential DCT (AC)", SOF),
+ 0xFFCE: ("SOF14", "Differential progressive DCT (AC)", SOF),
+ 0xFFCF: ("SOF15", "Differential spatial (AC)", SOF),
+ 0xFFD0: ("RST0", "Restart 0", None),
+ 0xFFD1: ("RST1", "Restart 1", None),
+ 0xFFD2: ("RST2", "Restart 2", None),
+ 0xFFD3: ("RST3", "Restart 3", None),
+ 0xFFD4: ("RST4", "Restart 4", None),
+ 0xFFD5: ("RST5", "Restart 5", None),
+ 0xFFD6: ("RST6", "Restart 6", None),
+ 0xFFD7: ("RST7", "Restart 7", None),
+ 0xFFD8: ("SOI", "Start of image", None),
+ 0xFFD9: ("EOI", "End of image", None),
+ 0xFFDA: ("SOS", "Start of scan", Skip),
+ 0xFFDB: ("DQT", "Define quantization table", DQT),
+ 0xFFDC: ("DNL", "Define number of lines", Skip),
+ 0xFFDD: ("DRI", "Define restart interval", Skip),
+ 0xFFDE: ("DHP", "Define hierarchical progression", SOF),
+ 0xFFDF: ("EXP", "Expand reference component", Skip),
+ 0xFFE0: ("APP0", "Application segment 0", APP),
+ 0xFFE1: ("APP1", "Application segment 1", APP),
+ 0xFFE2: ("APP2", "Application segment 2", APP),
+ 0xFFE3: ("APP3", "Application segment 3", APP),
+ 0xFFE4: ("APP4", "Application segment 4", APP),
+ 0xFFE5: ("APP5", "Application segment 5", APP),
+ 0xFFE6: ("APP6", "Application segment 6", APP),
+ 0xFFE7: ("APP7", "Application segment 7", APP),
+ 0xFFE8: ("APP8", "Application segment 8", APP),
+ 0xFFE9: ("APP9", "Application segment 9", APP),
+ 0xFFEA: ("APP10", "Application segment 10", APP),
+ 0xFFEB: ("APP11", "Application segment 11", APP),
+ 0xFFEC: ("APP12", "Application segment 12", APP),
+ 0xFFED: ("APP13", "Application segment 13", APP),
+ 0xFFEE: ("APP14", "Application segment 14", APP),
+ 0xFFEF: ("APP15", "Application segment 15", APP),
+ 0xFFF0: ("JPG0", "Extension 0", None),
+ 0xFFF1: ("JPG1", "Extension 1", None),
+ 0xFFF2: ("JPG2", "Extension 2", None),
+ 0xFFF3: ("JPG3", "Extension 3", None),
+ 0xFFF4: ("JPG4", "Extension 4", None),
+ 0xFFF5: ("JPG5", "Extension 5", None),
+ 0xFFF6: ("JPG6", "Extension 6", None),
+ 0xFFF7: ("JPG7", "Extension 7", None),
+ 0xFFF8: ("JPG8", "Extension 8", None),
+ 0xFFF9: ("JPG9", "Extension 9", None),
+ 0xFFFA: ("JPG10", "Extension 10", None),
+ 0xFFFB: ("JPG11", "Extension 11", None),
+ 0xFFFC: ("JPG12", "Extension 12", None),
+ 0xFFFD: ("JPG13", "Extension 13", None),
+ 0xFFFE: ("COM", "Comment", COM),
+}
+
+
+def _accept(prefix: bytes) -> bool:
+ # Magic number was taken from https://en.wikipedia.org/wiki/JPEG
+ return prefix.startswith(b"\xff\xd8\xff")
+
+
+##
+# Image plugin for JPEG and JFIF images.
+
+
+class JpegImageFile(ImageFile.ImageFile):
+ format = "JPEG"
+ format_description = "JPEG (ISO 10918)"
+
+ def _open(self) -> None:
+ assert self.fp is not None
+ s = self.fp.read(3)
+
+ if not _accept(s):
+ msg = "not a JPEG file"
+ raise SyntaxError(msg)
+ s = b"\xff"
+
+ # Create attributes
+ self.bits = self.layers = 0
+ self._exif_offset = 0
+
+ # JPEG specifics (internal)
+ self.layer: list[tuple[int, int, int, int]] = []
+ self._huffman_dc: dict[Any, Any] = {}
+ self._huffman_ac: dict[Any, Any] = {}
+ self.quantization: dict[int, list[int]] = {}
+ self.app: dict[str, bytes] = {} # compatibility
+ self.applist: list[tuple[str, bytes]] = []
+ self.icclist: list[bytes] = []
+
+ while True:
+ i = s[0]
+ if i == 0xFF:
+ s = s + self.fp.read(1)
+ i = i16(s)
+ else:
+ # Skip non-0xFF junk
+ s = self.fp.read(1)
+ continue
+
+ if i in MARKER:
+ name, description, handler = MARKER[i]
+ if handler is not None:
+ handler(self, i)
+ if i == 0xFFDA: # start of scan
+ rawmode = self.mode
+ if self.mode == "CMYK":
+ rawmode = "CMYK;I" # assume adobe conventions
+ self.tile = [
+ ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, ""))
+ ]
+ # self.__offset = self.fp.tell()
+ break
+ s = self.fp.read(1)
+ elif i in {0, 0xFFFF}:
+ # padded marker or junk; move on
+ s = b"\xff"
+ elif i == 0xFF00: # Skip extraneous data (escaped 0xFF)
+ s = self.fp.read(1)
+ else:
+ msg = "no marker found"
+ raise SyntaxError(msg)
+
+ self._read_dpi_from_exif()
+
+ def __getstate__(self) -> list[Any]:
+ return super().__getstate__() + [self.layers, self.layer]
+
+ def __setstate__(self, state: list[Any]) -> None:
+ self.layers, self.layer = state[6:]
+ super().__setstate__(state)
+
+ def load_read(self, read_bytes: int) -> bytes:
+ """
+ internal: read more image data
+ For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker
+ so libjpeg can finish decoding
+ """
+ assert self.fp is not None
+ s = self.fp.read(read_bytes)
+
+ if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"):
+ # Premature EOF.
+ # Pretend file is finished adding EOI marker
+ self._ended = True
+ return b"\xff\xd9"
+
+ return s
+
+ def draft(
+ self, mode: str | None, size: tuple[int, int] | None
+ ) -> tuple[str, tuple[int, int, float, float]] | None:
+ if len(self.tile) != 1:
+ return None
+
+ # Protect from second call
+ if self.decoderconfig:
+ return None
+
+ d, e, o, a = self.tile[0]
+ scale = 1
+ original_size = self.size
+
+ assert isinstance(a, tuple)
+ if a[0] == "RGB" and mode in ["L", "YCbCr"]:
+ self._mode = mode
+ a = mode, ""
+
+ if size:
+ scale = min(self.size[0] // size[0], self.size[1] // size[1])
+ for s in [8, 4, 2, 1]:
+ if scale >= s:
+ break
+ assert e is not None
+ e = (
+ e[0],
+ e[1],
+ (e[2] - e[0] + s - 1) // s + e[0],
+ (e[3] - e[1] + s - 1) // s + e[1],
+ )
+ self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s)
+ scale = s
+
+ self.tile = [ImageFile._Tile(d, e, o, a)]
+ self.decoderconfig = (scale, 0)
+
+ box = (0, 0, original_size[0] / scale, original_size[1] / scale)
+ return self.mode, box
+
+ def load_djpeg(self) -> None:
+ # ALTERNATIVE: handle JPEGs via the IJG command line utilities
+
+ f, path = tempfile.mkstemp()
+ os.close(f)
+ if os.path.exists(self.filename):
+ subprocess.check_call(["djpeg", "-outfile", path, self.filename])
+ else:
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+
+ msg = "Invalid Filename"
+ raise ValueError(msg)
+
+ try:
+ with Image.open(path) as _im:
+ _im.load()
+ self.im = _im.im
+ finally:
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+
+ self._mode = self.im.mode
+ self._size = self.im.size
+
+ self.tile = []
+
+ def _getexif(self) -> dict[int, Any] | None:
+ return _getexif(self)
+
+ def _read_dpi_from_exif(self) -> None:
+ # If DPI isn't in JPEG header, fetch from EXIF
+ if "dpi" in self.info or "exif" not in self.info:
+ return
+ try:
+ exif = self.getexif()
+ resolution_unit = exif[0x0128]
+ x_resolution = exif[0x011A]
+ try:
+ dpi = float(x_resolution[0]) / x_resolution[1]
+ except TypeError:
+ dpi = x_resolution
+ if math.isnan(dpi):
+ msg = "DPI is not a number"
+ raise ValueError(msg)
+ if resolution_unit == 3: # cm
+ # 1 dpcm = 2.54 dpi
+ dpi *= 2.54
+ self.info["dpi"] = dpi, dpi
+ except (
+ struct.error, # truncated EXIF
+ KeyError, # dpi not included
+ SyntaxError, # invalid/unreadable EXIF
+ TypeError, # dpi is an invalid float
+ ValueError, # dpi is an invalid float
+ ZeroDivisionError, # invalid dpi rational value
+ ):
+ self.info["dpi"] = 72, 72
+
+ def _getmp(self) -> dict[int, Any] | None:
+ return _getmp(self)
+
+
+def _getexif(self: JpegImageFile) -> dict[int, Any] | None:
+ if "exif" not in self.info:
+ return None
+ return self.getexif()._get_merged_dict()
+
+
+def _getmp(self: JpegImageFile) -> dict[int, Any] | None:
+ # Extract MP information. This method was inspired by the "highly
+ # experimental" _getexif version that's been in use for years now,
+ # itself based on the ImageFileDirectory class in the TIFF plugin.
+
+ # The MP record essentially consists of a TIFF file embedded in a JPEG
+ # application marker.
+ try:
+ data = self.info["mp"]
+ except KeyError:
+ return None
+ file_contents = io.BytesIO(data)
+ head = file_contents.read(8)
+ endianness = ">" if head.startswith(b"\x4d\x4d\x00\x2a") else "<"
+ # process dictionary
+ from . import TiffImagePlugin
+
+ try:
+ info = TiffImagePlugin.ImageFileDirectory_v2(head)
+ file_contents.seek(info.next)
+ info.load(file_contents)
+ mp = dict(info)
+ except Exception as e:
+ msg = "malformed MP Index (unreadable directory)"
+ raise SyntaxError(msg) from e
+ # it's an error not to have a number of images
+ try:
+ quant = mp[0xB001]
+ except KeyError as e:
+ msg = "malformed MP Index (no number of images)"
+ raise SyntaxError(msg) from e
+ # get MP entries
+ mpentries = []
+ try:
+ rawmpentries = mp[0xB002]
+ for entrynum in range(quant):
+ unpackedentry = struct.unpack_from(
+ f"{endianness}LLLHH", rawmpentries, entrynum * 16
+ )
+ labels = ("Attribute", "Size", "DataOffset", "EntryNo1", "EntryNo2")
+ mpentry = dict(zip(labels, unpackedentry))
+ mpentryattr = {
+ "DependentParentImageFlag": bool(mpentry["Attribute"] & (1 << 31)),
+ "DependentChildImageFlag": bool(mpentry["Attribute"] & (1 << 30)),
+ "RepresentativeImageFlag": bool(mpentry["Attribute"] & (1 << 29)),
+ "Reserved": (mpentry["Attribute"] & (3 << 27)) >> 27,
+ "ImageDataFormat": (mpentry["Attribute"] & (7 << 24)) >> 24,
+ "MPType": mpentry["Attribute"] & 0x00FFFFFF,
+ }
+ if mpentryattr["ImageDataFormat"] == 0:
+ mpentryattr["ImageDataFormat"] = "JPEG"
+ else:
+ msg = "unsupported picture format in MPO"
+ raise SyntaxError(msg)
+ mptypemap = {
+ 0x000000: "Undefined",
+ 0x010001: "Large Thumbnail (VGA Equivalent)",
+ 0x010002: "Large Thumbnail (Full HD Equivalent)",
+ 0x020001: "Multi-Frame Image (Panorama)",
+ 0x020002: "Multi-Frame Image: (Disparity)",
+ 0x020003: "Multi-Frame Image: (Multi-Angle)",
+ 0x030000: "Baseline MP Primary Image",
+ }
+ mpentryattr["MPType"] = mptypemap.get(mpentryattr["MPType"], "Unknown")
+ mpentry["Attribute"] = mpentryattr
+ mpentries.append(mpentry)
+ mp[0xB002] = mpentries
+ except KeyError as e:
+ msg = "malformed MP Index (bad MP Entry)"
+ raise SyntaxError(msg) from e
+ # Next we should try and parse the individual image unique ID list;
+ # we don't because I've never seen this actually used in a real MPO
+ # file and so can't test it.
+ return mp
+
+
+# --------------------------------------------------------------------
+# stuff to save JPEG files
+
+RAWMODE = {
+ "1": "L",
+ "L": "L",
+ "RGB": "RGB",
+ "RGBX": "RGB",
+ "CMYK": "CMYK;I", # assume adobe conventions
+ "YCbCr": "YCbCr",
+}
+
+# fmt: off
+zigzag_index = (
+ 0, 1, 5, 6, 14, 15, 27, 28,
+ 2, 4, 7, 13, 16, 26, 29, 42,
+ 3, 8, 12, 17, 25, 30, 41, 43,
+ 9, 11, 18, 24, 31, 40, 44, 53,
+ 10, 19, 23, 32, 39, 45, 52, 54,
+ 20, 22, 33, 38, 46, 51, 55, 60,
+ 21, 34, 37, 47, 50, 56, 59, 61,
+ 35, 36, 48, 49, 57, 58, 62, 63,
+)
+
+samplings = {
+ (1, 1, 1, 1, 1, 1): 0,
+ (2, 1, 1, 1, 1, 1): 1,
+ (2, 2, 1, 1, 1, 1): 2,
+}
+# fmt: on
+
+
+def get_sampling(im: Image.Image) -> int:
+ # There's no subsampling when images have only 1 layer
+ # (grayscale images) or when they are CMYK (4 layers),
+ # so set subsampling to the default value.
+ #
+ # NOTE: currently Pillow can't encode JPEG to YCCK format.
+ # If YCCK support is added in the future, subsampling code will have
+ # to be updated (here and in JpegEncode.c) to deal with 4 layers.
+ if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
+ return -1
+ sampling = im.layer[0][1:3] + im.layer[1][1:3] + im.layer[2][1:3]
+ return samplings.get(sampling, -1)
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ try:
+ rawmode = RAWMODE[im.mode]
+ except KeyError as e:
+ msg = f"cannot write mode {im.mode} as JPEG"
+ raise OSError(msg) from e
+
+ info = im.encoderinfo
+
+ dpi = [round(x) for x in info.get("dpi", (0, 0))]
+
+ quality = info.get("quality", -1)
+ subsampling = info.get("subsampling", -1)
+ qtables = info.get("qtables")
+
+ if quality == "keep":
+ quality = -1
+ subsampling = "keep"
+ qtables = "keep"
+ elif quality in presets:
+ preset = presets[quality]
+ quality = -1
+ subsampling = preset.get("subsampling", -1)
+ qtables = preset.get("quantization")
+ elif not isinstance(quality, int):
+ msg = "Invalid quality setting"
+ raise ValueError(msg)
+ else:
+ if subsampling in presets:
+ subsampling = presets[subsampling].get("subsampling", -1)
+ if isinstance(qtables, str) and qtables in presets:
+ qtables = presets[qtables].get("quantization")
+
+ if subsampling == "4:4:4":
+ subsampling = 0
+ elif subsampling == "4:2:2":
+ subsampling = 1
+ elif subsampling == "4:2:0":
+ subsampling = 2
+ elif subsampling == "4:1:1":
+ # For compatibility. Before Pillow 4.3, 4:1:1 actually meant 4:2:0.
+ # Set 4:2:0 if someone is still using that value.
+ subsampling = 2
+ elif subsampling == "keep":
+ if im.format != "JPEG":
+ msg = "Cannot use 'keep' when original image is not a JPEG"
+ raise ValueError(msg)
+ subsampling = get_sampling(im)
+
+ def validate_qtables(
+ qtables: (
+ str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None
+ ),
+ ) -> list[list[int]] | None:
+ if qtables is None:
+ return qtables
+ if isinstance(qtables, str):
+ try:
+ lines = [
+ int(num)
+ for line in qtables.splitlines()
+ for num in line.split("#", 1)[0].split()
+ ]
+ except ValueError as e:
+ msg = "Invalid quantization table"
+ raise ValueError(msg) from e
+ else:
+ qtables = [lines[s : s + 64] for s in range(0, len(lines), 64)]
+ if isinstance(qtables, (tuple, list, dict)):
+ if isinstance(qtables, dict):
+ qtables = [
+ qtables[key] for key in range(len(qtables)) if key in qtables
+ ]
+ elif isinstance(qtables, tuple):
+ qtables = list(qtables)
+ if not (0 < len(qtables) < 5):
+ msg = "None or too many quantization tables"
+ raise ValueError(msg)
+ try:
+ for idx, table in enumerate(qtables):
+ if len(table) != 64:
+ msg = "Invalid quantization table"
+ raise TypeError(msg)
+ qtables[idx] = list(array.array("H", table))
+ except TypeError as e:
+ msg = "Invalid quantization table"
+ raise ValueError(msg) from e
+ return qtables
+
+ if qtables == "keep":
+ if im.format != "JPEG":
+ msg = "Cannot use 'keep' when original image is not a JPEG"
+ raise ValueError(msg)
+ qtables = getattr(im, "quantization", None)
+ qtables = validate_qtables(qtables)
+
+ extra = info.get("extra", b"")
+
+ MAX_BYTES_IN_MARKER = 65533
+ if xmp := info.get("xmp"):
+ overhead_len = 29 # b"http://ns.adobe.com/xap/1.0/\x00"
+ max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
+ if len(xmp) > max_data_bytes_in_marker:
+ msg = "XMP data is too long"
+ raise ValueError(msg)
+ size = o16(2 + overhead_len + len(xmp))
+ extra += b"\xff\xe1" + size + b"http://ns.adobe.com/xap/1.0/\x00" + xmp
+
+ if icc_profile := info.get("icc_profile"):
+ overhead_len = 14 # b"ICC_PROFILE\0" + o8(i) + o8(len(markers))
+ max_data_bytes_in_marker = MAX_BYTES_IN_MARKER - overhead_len
+ markers = []
+ while icc_profile:
+ markers.append(icc_profile[:max_data_bytes_in_marker])
+ icc_profile = icc_profile[max_data_bytes_in_marker:]
+ i = 1
+ for marker in markers:
+ size = o16(2 + overhead_len + len(marker))
+ extra += (
+ b"\xff\xe2"
+ + size
+ + b"ICC_PROFILE\0"
+ + o8(i)
+ + o8(len(markers))
+ + marker
+ )
+ i += 1
+
+ comment = info.get("comment", im.info.get("comment"))
+
+ # "progressive" is the official name, but older documentation
+ # says "progression"
+ # FIXME: issue a warning if the wrong form is used (post-1.1.7)
+ progressive = info.get("progressive", False) or info.get("progression", False)
+
+ optimize = info.get("optimize", False)
+
+ exif = info.get("exif", b"")
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes()
+ if len(exif) > MAX_BYTES_IN_MARKER:
+ msg = "EXIF data is too long"
+ raise ValueError(msg)
+
+ # get keyword arguments
+ im.encoderconfig = (
+ quality,
+ progressive,
+ info.get("smooth", 0),
+ optimize,
+ info.get("keep_rgb", False),
+ info.get("streamtype", 0),
+ dpi,
+ subsampling,
+ info.get("restart_marker_blocks", 0),
+ info.get("restart_marker_rows", 0),
+ qtables,
+ comment,
+ extra,
+ exif,
+ )
+
+ # if we optimize, libjpeg needs a buffer big enough to hold the whole image
+ # in a shot. Guessing on the size, at im.size bytes. (raw pixel size is
+ # channels*size, this is a value that's been used in a django patch.
+ # https://github.com/matthewwithanm/django-imagekit/issues/50
+ if optimize or progressive:
+ # CMYK can be bigger
+ if im.mode == "CMYK":
+ bufsize = 4 * im.size[0] * im.size[1]
+ # keep sets quality to -1, but the actual value may be high.
+ elif quality >= 95 or quality == -1:
+ bufsize = 2 * im.size[0] * im.size[1]
+ else:
+ bufsize = im.size[0] * im.size[1]
+ if exif:
+ bufsize += len(exif) + 5
+ if extra:
+ bufsize += len(extra) + 1
+ else:
+ # The EXIF info needs to be written as one block, + APP1, + one spare byte.
+ # Ensure that our buffer is big enough. Same with the icc_profile block.
+ bufsize = max(len(exif) + 5, len(extra) + 1)
+
+ ImageFile._save(
+ im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize
+ )
+
+
+##
+# Factory for making JPEG and MPO instances
+def jpeg_factory(
+ fp: IO[bytes], filename: str | bytes | None = None
+) -> JpegImageFile | MpoImageFile:
+ im = JpegImageFile(fp, filename)
+ try:
+ mpheader = im._getmp()
+ if mpheader is not None and mpheader[45057] > 1:
+ for segment, content in im.applist:
+ if segment == "APP1" and b' hdrgm:Version="' in content:
+ # Ultra HDR images are not yet supported
+ return im
+ # It's actually an MPO
+ from .MpoImagePlugin import MpoImageFile
+
+ # Don't reload everything, just convert it.
+ im = MpoImageFile.adopt(im, mpheader)
+ except (TypeError, IndexError):
+ # It is really a JPEG
+ pass
+ except SyntaxError:
+ warnings.warn(
+ "Image appears to be a malformed MPO file, it will be "
+ "interpreted as a base JPEG file"
+ )
+ return im
+
+
+# ---------------------------------------------------------------------
+# Registry stuff
+
+Image.register_open(JpegImageFile.format, jpeg_factory, _accept)
+Image.register_save(JpegImageFile.format, _save)
+
+Image.register_extensions(JpegImageFile.format, [".jfif", ".jpe", ".jpg", ".jpeg"])
+
+Image.register_mime(JpegImageFile.format, "image/jpeg")
diff --git a/venv/lib/python3.11/site-packages/PIL/JpegPresets.py b/venv/lib/python3.11/site-packages/PIL/JpegPresets.py
new file mode 100644
index 000000000..d0e64a35e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/JpegPresets.py
@@ -0,0 +1,242 @@
+"""
+JPEG quality settings equivalent to the Photoshop settings.
+Can be used when saving JPEG files.
+
+The following presets are available by default:
+``web_low``, ``web_medium``, ``web_high``, ``web_very_high``, ``web_maximum``,
+``low``, ``medium``, ``high``, ``maximum``.
+More presets can be added to the :py:data:`presets` dict if needed.
+
+To apply the preset, specify::
+
+ quality="preset_name"
+
+To apply only the quantization table::
+
+ qtables="preset_name"
+
+To apply only the subsampling setting::
+
+ subsampling="preset_name"
+
+Example::
+
+ im.save("image_name.jpg", quality="web_high")
+
+Subsampling
+-----------
+
+Subsampling is the practice of encoding images by implementing less resolution
+for chroma information than for luma information.
+(ref.: https://en.wikipedia.org/wiki/Chroma_subsampling)
+
+Possible subsampling values are 0, 1 and 2 that correspond to 4:4:4, 4:2:2 and
+4:2:0.
+
+You can get the subsampling of a JPEG with the
+:func:`.JpegImagePlugin.get_sampling` function.
+
+In JPEG compressed data a JPEG marker is used instead of an EXIF tag.
+(ref.: https://exiv2.org/tags.html)
+
+
+Quantization tables
+-------------------
+
+They are values use by the DCT (Discrete cosine transform) to remove
+*unnecessary* information from the image (the lossy part of the compression).
+(ref.: https://en.wikipedia.org/wiki/Quantization_matrix#Quantization_matrices,
+https://en.wikipedia.org/wiki/JPEG#Quantization)
+
+You can get the quantization tables of a JPEG with::
+
+ im.quantization
+
+This will return a dict with a number of lists. You can pass this dict
+directly as the qtables argument when saving a JPEG.
+
+The quantization table format in presets is a list with sublists. These formats
+are interchangeable.
+
+Libjpeg ref.:
+https://web.archive.org/web/20120328125543/http://www.jpegcameras.com/libjpeg/libjpeg-3.html
+
+"""
+
+from __future__ import annotations
+
+# fmt: off
+presets = {
+ 'web_low': {'subsampling': 2, # "4:2:0"
+ 'quantization': [
+ [20, 16, 25, 39, 50, 46, 62, 68,
+ 16, 18, 23, 38, 38, 53, 65, 68,
+ 25, 23, 31, 38, 53, 65, 68, 68,
+ 39, 38, 38, 53, 65, 68, 68, 68,
+ 50, 38, 53, 65, 68, 68, 68, 68,
+ 46, 53, 65, 68, 68, 68, 68, 68,
+ 62, 65, 68, 68, 68, 68, 68, 68,
+ 68, 68, 68, 68, 68, 68, 68, 68],
+ [21, 25, 32, 38, 54, 68, 68, 68,
+ 25, 28, 24, 38, 54, 68, 68, 68,
+ 32, 24, 32, 43, 66, 68, 68, 68,
+ 38, 38, 43, 53, 68, 68, 68, 68,
+ 54, 54, 66, 68, 68, 68, 68, 68,
+ 68, 68, 68, 68, 68, 68, 68, 68,
+ 68, 68, 68, 68, 68, 68, 68, 68,
+ 68, 68, 68, 68, 68, 68, 68, 68]
+ ]},
+ 'web_medium': {'subsampling': 2, # "4:2:0"
+ 'quantization': [
+ [16, 11, 11, 16, 23, 27, 31, 30,
+ 11, 12, 12, 15, 20, 23, 23, 30,
+ 11, 12, 13, 16, 23, 26, 35, 47,
+ 16, 15, 16, 23, 26, 37, 47, 64,
+ 23, 20, 23, 26, 39, 51, 64, 64,
+ 27, 23, 26, 37, 51, 64, 64, 64,
+ 31, 23, 35, 47, 64, 64, 64, 64,
+ 30, 30, 47, 64, 64, 64, 64, 64],
+ [17, 15, 17, 21, 20, 26, 38, 48,
+ 15, 19, 18, 17, 20, 26, 35, 43,
+ 17, 18, 20, 22, 26, 30, 46, 53,
+ 21, 17, 22, 28, 30, 39, 53, 64,
+ 20, 20, 26, 30, 39, 48, 64, 64,
+ 26, 26, 30, 39, 48, 63, 64, 64,
+ 38, 35, 46, 53, 64, 64, 64, 64,
+ 48, 43, 53, 64, 64, 64, 64, 64]
+ ]},
+ 'web_high': {'subsampling': 0, # "4:4:4"
+ 'quantization': [
+ [6, 4, 4, 6, 9, 11, 12, 16,
+ 4, 5, 5, 6, 8, 10, 12, 12,
+ 4, 5, 5, 6, 10, 12, 14, 19,
+ 6, 6, 6, 11, 12, 15, 19, 28,
+ 9, 8, 10, 12, 16, 20, 27, 31,
+ 11, 10, 12, 15, 20, 27, 31, 31,
+ 12, 12, 14, 19, 27, 31, 31, 31,
+ 16, 12, 19, 28, 31, 31, 31, 31],
+ [7, 7, 13, 24, 26, 31, 31, 31,
+ 7, 12, 16, 21, 31, 31, 31, 31,
+ 13, 16, 17, 31, 31, 31, 31, 31,
+ 24, 21, 31, 31, 31, 31, 31, 31,
+ 26, 31, 31, 31, 31, 31, 31, 31,
+ 31, 31, 31, 31, 31, 31, 31, 31,
+ 31, 31, 31, 31, 31, 31, 31, 31,
+ 31, 31, 31, 31, 31, 31, 31, 31]
+ ]},
+ 'web_very_high': {'subsampling': 0, # "4:4:4"
+ 'quantization': [
+ [2, 2, 2, 2, 3, 4, 5, 6,
+ 2, 2, 2, 2, 3, 4, 5, 6,
+ 2, 2, 2, 2, 4, 5, 7, 9,
+ 2, 2, 2, 4, 5, 7, 9, 12,
+ 3, 3, 4, 5, 8, 10, 12, 12,
+ 4, 4, 5, 7, 10, 12, 12, 12,
+ 5, 5, 7, 9, 12, 12, 12, 12,
+ 6, 6, 9, 12, 12, 12, 12, 12],
+ [3, 3, 5, 9, 13, 15, 15, 15,
+ 3, 4, 6, 11, 14, 12, 12, 12,
+ 5, 6, 9, 14, 12, 12, 12, 12,
+ 9, 11, 14, 12, 12, 12, 12, 12,
+ 13, 14, 12, 12, 12, 12, 12, 12,
+ 15, 12, 12, 12, 12, 12, 12, 12,
+ 15, 12, 12, 12, 12, 12, 12, 12,
+ 15, 12, 12, 12, 12, 12, 12, 12]
+ ]},
+ 'web_maximum': {'subsampling': 0, # "4:4:4"
+ 'quantization': [
+ [1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 1,
+ 1, 1, 1, 1, 1, 1, 1, 2,
+ 1, 1, 1, 1, 1, 1, 2, 2,
+ 1, 1, 1, 1, 1, 2, 2, 3,
+ 1, 1, 1, 1, 2, 2, 3, 3,
+ 1, 1, 1, 2, 2, 3, 3, 3,
+ 1, 1, 2, 2, 3, 3, 3, 3],
+ [1, 1, 1, 2, 2, 3, 3, 3,
+ 1, 1, 1, 2, 3, 3, 3, 3,
+ 1, 1, 1, 3, 3, 3, 3, 3,
+ 2, 2, 3, 3, 3, 3, 3, 3,
+ 2, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 3, 3]
+ ]},
+ 'low': {'subsampling': 2, # "4:2:0"
+ 'quantization': [
+ [18, 14, 14, 21, 30, 35, 34, 17,
+ 14, 16, 16, 19, 26, 23, 12, 12,
+ 14, 16, 17, 21, 23, 12, 12, 12,
+ 21, 19, 21, 23, 12, 12, 12, 12,
+ 30, 26, 23, 12, 12, 12, 12, 12,
+ 35, 23, 12, 12, 12, 12, 12, 12,
+ 34, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12],
+ [20, 19, 22, 27, 20, 20, 17, 17,
+ 19, 25, 23, 14, 14, 12, 12, 12,
+ 22, 23, 14, 14, 12, 12, 12, 12,
+ 27, 14, 14, 12, 12, 12, 12, 12,
+ 20, 14, 12, 12, 12, 12, 12, 12,
+ 20, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12]
+ ]},
+ 'medium': {'subsampling': 2, # "4:2:0"
+ 'quantization': [
+ [12, 8, 8, 12, 17, 21, 24, 17,
+ 8, 9, 9, 11, 15, 19, 12, 12,
+ 8, 9, 10, 12, 19, 12, 12, 12,
+ 12, 11, 12, 21, 12, 12, 12, 12,
+ 17, 15, 19, 12, 12, 12, 12, 12,
+ 21, 19, 12, 12, 12, 12, 12, 12,
+ 24, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12],
+ [13, 11, 13, 16, 20, 20, 17, 17,
+ 11, 14, 14, 14, 14, 12, 12, 12,
+ 13, 14, 14, 14, 12, 12, 12, 12,
+ 16, 14, 14, 12, 12, 12, 12, 12,
+ 20, 14, 12, 12, 12, 12, 12, 12,
+ 20, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12]
+ ]},
+ 'high': {'subsampling': 0, # "4:4:4"
+ 'quantization': [
+ [6, 4, 4, 6, 9, 11, 12, 16,
+ 4, 5, 5, 6, 8, 10, 12, 12,
+ 4, 5, 5, 6, 10, 12, 12, 12,
+ 6, 6, 6, 11, 12, 12, 12, 12,
+ 9, 8, 10, 12, 12, 12, 12, 12,
+ 11, 10, 12, 12, 12, 12, 12, 12,
+ 12, 12, 12, 12, 12, 12, 12, 12,
+ 16, 12, 12, 12, 12, 12, 12, 12],
+ [7, 7, 13, 24, 20, 20, 17, 17,
+ 7, 12, 16, 14, 14, 12, 12, 12,
+ 13, 16, 14, 14, 12, 12, 12, 12,
+ 24, 14, 14, 12, 12, 12, 12, 12,
+ 20, 14, 12, 12, 12, 12, 12, 12,
+ 20, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12,
+ 17, 12, 12, 12, 12, 12, 12, 12]
+ ]},
+ 'maximum': {'subsampling': 0, # "4:4:4"
+ 'quantization': [
+ [2, 2, 2, 2, 3, 4, 5, 6,
+ 2, 2, 2, 2, 3, 4, 5, 6,
+ 2, 2, 2, 2, 4, 5, 7, 9,
+ 2, 2, 2, 4, 5, 7, 9, 12,
+ 3, 3, 4, 5, 8, 10, 12, 12,
+ 4, 4, 5, 7, 10, 12, 12, 12,
+ 5, 5, 7, 9, 12, 12, 12, 12,
+ 6, 6, 9, 12, 12, 12, 12, 12],
+ [3, 3, 5, 9, 13, 15, 15, 15,
+ 3, 4, 6, 10, 14, 12, 12, 12,
+ 5, 6, 9, 14, 12, 12, 12, 12,
+ 9, 10, 14, 12, 12, 12, 12, 12,
+ 13, 14, 12, 12, 12, 12, 12, 12,
+ 15, 12, 12, 12, 12, 12, 12, 12,
+ 15, 12, 12, 12, 12, 12, 12, 12,
+ 15, 12, 12, 12, 12, 12, 12, 12]
+ ]},
+}
+# fmt: on
diff --git a/venv/lib/python3.11/site-packages/PIL/McIdasImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/McIdasImagePlugin.py
new file mode 100644
index 000000000..9a47933b6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/McIdasImagePlugin.py
@@ -0,0 +1,78 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# Basic McIdas support for PIL
+#
+# History:
+# 1997-05-05 fl Created (8-bit images only)
+# 2009-03-08 fl Added 16/32-bit support.
+#
+# Thanks to Richard Jones and Craig Swank for specs and samples.
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1997.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import struct
+
+from . import Image, ImageFile
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(b"\x00\x00\x00\x00\x00\x00\x00\x04")
+
+
+##
+# Image plugin for McIdas area images.
+
+
+class McIdasImageFile(ImageFile.ImageFile):
+ format = "MCIDAS"
+ format_description = "McIdas area file"
+
+ def _open(self) -> None:
+ # parse area file directory
+ assert self.fp is not None
+
+ s = self.fp.read(256)
+ if not _accept(s) or len(s) != 256:
+ msg = "not an McIdas area file"
+ raise SyntaxError(msg)
+
+ self.area_descriptor_raw = s
+ self.area_descriptor = w = [0, *struct.unpack("!64i", s)]
+
+ # get mode
+ if w[11] == 1:
+ mode = rawmode = "L"
+ elif w[11] == 2:
+ mode = rawmode = "I;16B"
+ elif w[11] == 4:
+ # FIXME: add memory map support
+ mode = "I"
+ rawmode = "I;32B"
+ else:
+ msg = "unsupported McIdas format"
+ raise SyntaxError(msg)
+
+ self._mode = mode
+ self._size = w[10], w[9]
+
+ offset = w[34] + w[15]
+ stride = w[15] + w[10] * w[11] * w[14]
+
+ self.tile = [
+ ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))
+ ]
+
+
+# --------------------------------------------------------------------
+# registry
+
+Image.register_open(McIdasImageFile.format, McIdasImageFile, _accept)
+
+# no default extension
diff --git a/venv/lib/python3.11/site-packages/PIL/MicImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/MicImagePlugin.py
new file mode 100644
index 000000000..99a07bae0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/MicImagePlugin.py
@@ -0,0 +1,103 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# Microsoft Image Composer support for PIL
+#
+# Notes:
+# uses TiffImagePlugin.py to read the actual image streams
+#
+# History:
+# 97-01-20 fl Created
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1997.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import olefile
+
+from . import Image, TiffImagePlugin
+
+#
+# --------------------------------------------------------------------
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(olefile.MAGIC)
+
+
+##
+# Image plugin for Microsoft's Image Composer file format.
+
+
+class MicImageFile(TiffImagePlugin.TiffImageFile):
+ format = "MIC"
+ format_description = "Microsoft Image Composer"
+ _close_exclusive_fp_after_loading = False
+
+ def _open(self) -> None:
+ # read the OLE directory and see if this is a likely
+ # to be a Microsoft Image Composer file
+
+ try:
+ self.ole = olefile.OleFileIO(self.fp)
+ except OSError as e:
+ msg = "not an MIC file; invalid OLE file"
+ raise SyntaxError(msg) from e
+
+ # find ACI subfiles with Image members (maybe not the
+ # best way to identify MIC files, but what the... ;-)
+
+ self.images = [
+ path
+ for path in self.ole.listdir()
+ if path[1:] and path[0].endswith(".ACI") and path[1] == "Image"
+ ]
+
+ # if we didn't find any images, this is probably not
+ # an MIC file.
+ if not self.images:
+ msg = "not an MIC file; no image entries"
+ raise SyntaxError(msg)
+
+ self.frame = -1
+ self._n_frames = len(self.images)
+ self.is_animated = self._n_frames > 1
+
+ assert self.fp is not None
+ self.__fp = self.fp
+ self.seek(0)
+
+ def seek(self, frame: int) -> None:
+ if not self._seek_check(frame):
+ return
+ filename = self.images[frame]
+ self.fp = self.ole.openstream(filename)
+
+ TiffImagePlugin.TiffImageFile._open(self)
+
+ self.frame = frame
+
+ def tell(self) -> int:
+ return self.frame
+
+ def close(self) -> None:
+ self.__fp.close()
+ self.ole.close()
+ super().close()
+
+ def __exit__(self, *args: object) -> None:
+ self.__fp.close()
+ self.ole.close()
+ super().__exit__()
+
+
+#
+# --------------------------------------------------------------------
+
+Image.register_open(MicImageFile.format, MicImageFile, _accept)
+
+Image.register_extension(MicImageFile.format, ".mic")
diff --git a/venv/lib/python3.11/site-packages/PIL/MpegImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/MpegImagePlugin.py
new file mode 100644
index 000000000..47ebe9d62
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/MpegImagePlugin.py
@@ -0,0 +1,84 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# MPEG file handling
+#
+# History:
+# 95-09-09 fl Created
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1995.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from . import Image, ImageFile
+from ._binary import i8
+from ._typing import SupportsRead
+
+#
+# Bitstream parser
+
+
+class BitStream:
+ def __init__(self, fp: SupportsRead[bytes]) -> None:
+ self.fp = fp
+ self.bits = 0
+ self.bitbuffer = 0
+
+ def next(self) -> int:
+ return i8(self.fp.read(1))
+
+ def peek(self, bits: int) -> int:
+ while self.bits < bits:
+ self.bitbuffer = (self.bitbuffer << 8) + self.next()
+ self.bits += 8
+ return self.bitbuffer >> (self.bits - bits) & (1 << bits) - 1
+
+ def skip(self, bits: int) -> None:
+ while self.bits < bits:
+ self.bitbuffer = (self.bitbuffer << 8) + i8(self.fp.read(1))
+ self.bits += 8
+ self.bits = self.bits - bits
+
+ def read(self, bits: int) -> int:
+ v = self.peek(bits)
+ self.bits = self.bits - bits
+ return v
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(b"\x00\x00\x01\xb3")
+
+
+##
+# Image plugin for MPEG streams. This plugin can identify a stream,
+# but it cannot read it.
+
+
+class MpegImageFile(ImageFile.ImageFile):
+ format = "MPEG"
+ format_description = "MPEG"
+
+ def _open(self) -> None:
+ assert self.fp is not None
+
+ s = BitStream(self.fp)
+ if s.read(32) != 0x1B3:
+ msg = "not an MPEG file"
+ raise SyntaxError(msg)
+
+ self._mode = "RGB"
+ self._size = s.read(12), s.read(12)
+
+
+# --------------------------------------------------------------------
+# Registry stuff
+
+Image.register_open(MpegImageFile.format, MpegImageFile, _accept)
+
+Image.register_extensions(MpegImageFile.format, [".mpg", ".mpeg"])
+
+Image.register_mime(MpegImageFile.format, "video/mpeg")
diff --git a/venv/lib/python3.11/site-packages/PIL/MpoImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/MpoImagePlugin.py
new file mode 100644
index 000000000..bee0a56f9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/MpoImagePlugin.py
@@ -0,0 +1,203 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# MPO file handling
+#
+# See "Multi-Picture Format" (CIPA DC-007-Translation 2009, Standard of the
+# Camera & Imaging Products Association)
+#
+# The multi-picture object combines multiple JPEG images (with a modified EXIF
+# data format) into a single file. While it can theoretically be used much like
+# a GIF animation, it is commonly used to represent 3D photographs and is (as
+# of this writing) the most commonly used format by 3D cameras.
+#
+# History:
+# 2014-03-13 Feneric Created
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import os
+import struct
+from typing import IO, Any, cast
+
+from . import (
+ Image,
+ ImageFile,
+ ImageSequence,
+ JpegImagePlugin,
+ TiffImagePlugin,
+)
+from ._binary import o32le
+from ._util import DeferredError
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ JpegImagePlugin._save(im, fp, filename)
+
+
+def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ append_images = im.encoderinfo.get("append_images", [])
+ if not append_images and not getattr(im, "is_animated", False):
+ _save(im, fp, filename)
+ return
+
+ mpf_offset = 28
+ offsets: list[int] = []
+ im_sequences = [im, *append_images]
+ total = sum(getattr(seq, "n_frames", 1) for seq in im_sequences)
+ for im_sequence in im_sequences:
+ for im_frame in ImageSequence.Iterator(im_sequence):
+ if not offsets:
+ # APP2 marker
+ ifd_length = 66 + 16 * total
+ im_frame.encoderinfo["extra"] = (
+ b"\xff\xe2"
+ + struct.pack(">H", 6 + ifd_length)
+ + b"MPF\0"
+ + b" " * ifd_length
+ )
+ if exif := im_frame.encoderinfo.get("exif"):
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes()
+ im_frame.encoderinfo["exif"] = exif
+ mpf_offset += 4 + len(exif)
+
+ JpegImagePlugin._save(im_frame, fp, filename)
+ offsets.append(fp.tell())
+ else:
+ encoderinfo = im_frame._attach_default_encoderinfo(im)
+ im_frame.save(fp, "JPEG")
+ im_frame.encoderinfo = encoderinfo
+ offsets.append(fp.tell() - offsets[-1])
+
+ ifd = TiffImagePlugin.ImageFileDirectory_v2()
+ ifd[0xB000] = b"0100"
+ ifd[0xB001] = len(offsets)
+
+ mpentries = b""
+ data_offset = 0
+ for i, size in enumerate(offsets):
+ if i == 0:
+ mptype = 0x030000 # Baseline MP Primary Image
+ else:
+ mptype = 0x000000 # Undefined
+ mpentries += struct.pack(" None:
+ assert self.fp is not None
+ self.fp.seek(0) # prep the fp in order to pass the JPEG test
+ JpegImagePlugin.JpegImageFile._open(self)
+ self._after_jpeg_open()
+
+ def _after_jpeg_open(self, mpheader: dict[int, Any] | None = None) -> None:
+ self.mpinfo = mpheader if mpheader is not None else self._getmp()
+ if self.mpinfo is None:
+ msg = "Image appears to be a malformed MPO file"
+ raise ValueError(msg)
+ self.n_frames = self.mpinfo[0xB001]
+ self.__mpoffsets = [
+ mpent["DataOffset"] + self.info["mpoffset"] for mpent in self.mpinfo[0xB002]
+ ]
+ self.__mpoffsets[0] = 0
+ # Note that the following assertion will only be invalid if something
+ # gets broken within JpegImagePlugin.
+ assert self.n_frames == len(self.__mpoffsets)
+ del self.info["mpoffset"] # no longer needed
+ self.is_animated = self.n_frames > 1
+ assert self.fp is not None
+ self._fp = self.fp # FIXME: hack
+ self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame
+ self.__frame = 0
+ self.offset = 0
+ # for now we can only handle reading and individual frame extraction
+ self.readonly = 1
+
+ def load_seek(self, pos: int) -> None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
+ self._fp.seek(pos)
+
+ def seek(self, frame: int) -> None:
+ if not self._seek_check(frame):
+ return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
+ self.fp = self._fp
+ self.offset = self.__mpoffsets[frame]
+
+ original_exif = self.info.get("exif")
+ if "exif" in self.info:
+ del self.info["exif"]
+
+ self.fp.seek(self.offset + 2) # skip SOI marker
+ if not self.fp.read(2):
+ msg = "No data found for frame"
+ raise ValueError(msg)
+ self.fp.seek(self.offset)
+ JpegImagePlugin.JpegImageFile._open(self)
+ if self.info.get("exif") != original_exif:
+ self._reload_exif()
+
+ self.tile = [
+ ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
+ ]
+ self.__frame = frame
+
+ def tell(self) -> int:
+ return self.__frame
+
+ @staticmethod
+ def adopt(
+ jpeg_instance: JpegImagePlugin.JpegImageFile,
+ mpheader: dict[int, Any] | None = None,
+ ) -> MpoImageFile:
+ """
+ Transform the instance of JpegImageFile into
+ an instance of MpoImageFile.
+ After the call, the JpegImageFile is extended
+ to be an MpoImageFile.
+
+ This is essentially useful when opening a JPEG
+ file that reveals itself as an MPO, to avoid
+ double call to _open.
+ """
+ jpeg_instance.__class__ = MpoImageFile
+ mpo_instance = cast(MpoImageFile, jpeg_instance)
+ mpo_instance._after_jpeg_open(mpheader)
+ return mpo_instance
+
+
+# ---------------------------------------------------------------------
+# Registry stuff
+
+# Note that since MPO shares a factory with JPEG, we do not need to do a
+# separate registration for it here.
+# Image.register_open(MpoImageFile.format,
+# JpegImagePlugin.jpeg_factory, _accept)
+Image.register_save(MpoImageFile.format, _save)
+Image.register_save_all(MpoImageFile.format, _save_all)
+
+Image.register_extension(MpoImageFile.format, ".mpo")
+
+Image.register_mime(MpoImageFile.format, "image/mpo")
diff --git a/venv/lib/python3.11/site-packages/PIL/MspImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/MspImagePlugin.py
new file mode 100644
index 000000000..9df5cfd93
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/MspImagePlugin.py
@@ -0,0 +1,200 @@
+#
+# The Python Imaging Library.
+#
+# MSP file handling
+#
+# This is the format used by the Paint program in Windows 1 and 2.
+#
+# History:
+# 95-09-05 fl Created
+# 97-01-03 fl Read/write MSP images
+# 17-02-21 es Fixed RLE interpretation
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1995-97.
+# Copyright (c) Eric Soroos 2017.
+#
+# See the README file for information on usage and redistribution.
+#
+# More info on this format: https://archive.org/details/gg243631
+# Page 313:
+# Figure 205. Windows Paint Version 1: "DanM" Format
+# Figure 206. Windows Paint Version 2: "LinS" Format. Used in Windows V2.03
+#
+# See also: https://www.fileformat.info/format/mspaint/egff.htm
+from __future__ import annotations
+
+import io
+import struct
+from typing import IO
+
+from . import Image, ImageFile
+from ._binary import i16le as i16
+from ._binary import o16le as o16
+
+#
+# read MSP files
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith((b"DanM", b"LinS"))
+
+
+##
+# Image plugin for Windows MSP images. This plugin supports both
+# uncompressed (Windows 1.0).
+
+
+class MspImageFile(ImageFile.ImageFile):
+ format = "MSP"
+ format_description = "Windows Paint"
+
+ def _open(self) -> None:
+ # Header
+ assert self.fp is not None
+
+ s = self.fp.read(32)
+ if not _accept(s):
+ msg = "not an MSP file"
+ raise SyntaxError(msg)
+
+ # Header checksum
+ checksum = 0
+ for i in range(0, 32, 2):
+ checksum = checksum ^ i16(s, i)
+ if checksum != 0:
+ msg = "bad MSP checksum"
+ raise SyntaxError(msg)
+
+ self._mode = "1"
+ self._size = i16(s, 4), i16(s, 6)
+
+ if s.startswith(b"DanM"):
+ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, "1")]
+ else:
+ self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32)]
+
+
+class MspDecoder(ImageFile.PyDecoder):
+ # The algo for the MSP decoder is from
+ # https://www.fileformat.info/format/mspaint/egff.htm
+ # cc-by-attribution -- That page references is taken from the
+ # Encyclopedia of Graphics File Formats and is licensed by
+ # O'Reilly under the Creative Common/Attribution license
+ #
+ # For RLE encoded files, the 32byte header is followed by a scan
+ # line map, encoded as one 16bit word of encoded byte length per
+ # line.
+ #
+ # NOTE: the encoded length of the line can be 0. This was not
+ # handled in the previous version of this encoder, and there's no
+ # mention of how to handle it in the documentation. From the few
+ # examples I've seen, I've assumed that it is a fill of the
+ # background color, in this case, white.
+ #
+ #
+ # Pseudocode of the decoder:
+ # Read a BYTE value as the RunType
+ # If the RunType value is zero
+ # Read next byte as the RunCount
+ # Read the next byte as the RunValue
+ # Write the RunValue byte RunCount times
+ # If the RunType value is non-zero
+ # Use this value as the RunCount
+ # Read and write the next RunCount bytes literally
+ #
+ # e.g.:
+ # 0x00 03 ff 05 00 01 02 03 04
+ # would yield the bytes:
+ # 0xff ff ff 00 01 02 03 04
+ #
+ # which are then interpreted as a bit packed mode '1' image
+
+ _pulls_fd = True
+
+ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
+ assert self.fd is not None
+
+ img = io.BytesIO()
+ blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8))
+ try:
+ self.fd.seek(32)
+ rowmap = struct.unpack_from(
+ f"<{self.state.ysize}H", self.fd.read(self.state.ysize * 2)
+ )
+ except struct.error as e:
+ msg = "Truncated MSP file in row map"
+ raise OSError(msg) from e
+
+ for x, rowlen in enumerate(rowmap):
+ try:
+ if rowlen == 0:
+ img.write(blank_line)
+ continue
+ row = self.fd.read(rowlen)
+ if len(row) != rowlen:
+ msg = f"Truncated MSP file, expected {rowlen} bytes on row {x}"
+ raise OSError(msg)
+ idx = 0
+ while idx < rowlen:
+ runtype = row[idx]
+ idx += 1
+ if runtype == 0:
+ runcount, runval = struct.unpack_from("Bc", row, idx)
+ img.write(runval * runcount)
+ idx += 2
+ else:
+ runcount = runtype
+ img.write(row[idx : idx + runcount])
+ idx += runcount
+
+ except struct.error as e:
+ msg = f"Corrupted MSP file in row {x}"
+ raise OSError(msg) from e
+
+ self.set_as_raw(img.getvalue(), "1")
+
+ return -1, 0
+
+
+Image.register_decoder("MSP", MspDecoder)
+
+
+#
+# write MSP files (uncompressed only)
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode != "1":
+ msg = f"cannot write mode {im.mode} as MSP"
+ raise OSError(msg)
+
+ # create MSP header
+ header = [0] * 16
+
+ header[0], header[1] = i16(b"Da"), i16(b"nM") # version 1
+ header[2], header[3] = im.size
+ header[4], header[5] = 1, 1
+ header[6], header[7] = 1, 1
+ header[8], header[9] = im.size
+
+ checksum = 0
+ for h in header:
+ checksum = checksum ^ h
+ header[12] = checksum # FIXME: is this the right field?
+
+ # header
+ for h in header:
+ fp.write(o16(h))
+
+ # image body
+ ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, "1")])
+
+
+#
+# registry
+
+Image.register_open(MspImageFile.format, MspImageFile, _accept)
+Image.register_save(MspImageFile.format, _save)
+
+Image.register_extension(MspImageFile.format, ".msp")
diff --git a/venv/lib/python3.11/site-packages/PIL/PSDraw.py b/venv/lib/python3.11/site-packages/PIL/PSDraw.py
new file mode 100644
index 000000000..e6b74a918
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PSDraw.py
@@ -0,0 +1,238 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# Simple PostScript graphics interface
+#
+# History:
+# 1996-04-20 fl Created
+# 1999-01-10 fl Added gsave/grestore to image method
+# 2005-05-04 fl Fixed floating point issue in image (from Eric Etheridge)
+#
+# Copyright (c) 1997-2005 by Secret Labs AB. All rights reserved.
+# Copyright (c) 1996 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import sys
+from typing import IO
+
+from . import EpsImagePlugin
+
+TYPE_CHECKING = False
+
+
+##
+# Simple PostScript graphics interface.
+
+
+class PSDraw:
+ """
+ Sets up printing to the given file. If ``fp`` is omitted,
+ ``sys.stdout.buffer`` is assumed.
+ """
+
+ def __init__(self, fp: IO[bytes] | None = None) -> None:
+ if not fp:
+ fp = sys.stdout.buffer
+ self.fp = fp
+
+ def begin_document(self, id: str | None = None) -> None:
+ """Set up printing of a document. (Write PostScript DSC header.)"""
+ # FIXME: incomplete
+ self.fp.write(
+ b"%!PS-Adobe-3.0\n"
+ b"save\n"
+ b"/showpage { } def\n"
+ b"%%EndComments\n"
+ b"%%BeginDocument\n"
+ )
+ # self.fp.write(ERROR_PS) # debugging!
+ self.fp.write(EDROFF_PS)
+ self.fp.write(VDI_PS)
+ self.fp.write(b"%%EndProlog\n")
+ self.isofont: dict[bytes, int] = {}
+
+ def end_document(self) -> None:
+ """Ends printing. (Write PostScript DSC footer.)"""
+ self.fp.write(b"%%EndDocument\nrestore showpage\n%%End\n")
+ if hasattr(self.fp, "flush"):
+ self.fp.flush()
+
+ def setfont(self, font: str, size: int) -> None:
+ """
+ Selects which font to use.
+
+ :param font: A PostScript font name
+ :param size: Size in points.
+ """
+ font_bytes = bytes(font, "UTF-8")
+ if font_bytes not in self.isofont:
+ # reencode font
+ self.fp.write(
+ b"/PSDraw-%s ISOLatin1Encoding /%s E\n" % (font_bytes, font_bytes)
+ )
+ self.isofont[font_bytes] = 1
+ # rough
+ self.fp.write(b"/F0 %d /PSDraw-%s F\n" % (size, font_bytes))
+
+ def line(self, xy0: tuple[int, int], xy1: tuple[int, int]) -> None:
+ """
+ Draws a line between the two points. Coordinates are given in
+ PostScript point coordinates (72 points per inch, (0, 0) is the lower
+ left corner of the page).
+ """
+ self.fp.write(b"%d %d %d %d Vl\n" % (*xy0, *xy1))
+
+ def rectangle(self, box: tuple[int, int, int, int]) -> None:
+ """
+ Draws a rectangle.
+
+ :param box: A tuple of four integers, specifying left, bottom, width and
+ height.
+ """
+ self.fp.write(b"%d %d M 0 %d %d Vr\n" % box)
+
+ def text(self, xy: tuple[int, int], text: str) -> None:
+ """
+ Draws text at the given position. You must use
+ :py:meth:`~PIL.PSDraw.PSDraw.setfont` before calling this method.
+ """
+ # The font is loaded as ISOLatin1Encoding, so use latin-1 here.
+ text_bytes = bytes(text, "latin-1")
+ text_bytes = b"\\(".join(text_bytes.split(b"("))
+ text_bytes = b"\\)".join(text_bytes.split(b")"))
+ self.fp.write(b"%d %d M (%s) S\n" % (xy + (text_bytes,)))
+
+ if TYPE_CHECKING:
+ from . import Image
+
+ def image(
+ self, box: tuple[int, int, int, int], im: Image.Image, dpi: int | None = None
+ ) -> None:
+ """Draw a PIL image, centered in the given box."""
+ # default resolution depends on mode
+ if not dpi:
+ if im.mode == "1":
+ dpi = 200 # fax
+ else:
+ dpi = 100 # grayscale
+ # image size (on paper)
+ x = im.size[0] * 72 / dpi
+ y = im.size[1] * 72 / dpi
+ # max allowed size
+ xmax = float(box[2] - box[0])
+ ymax = float(box[3] - box[1])
+ if x > xmax:
+ y = y * xmax / x
+ x = xmax
+ if y > ymax:
+ x = x * ymax / y
+ y = ymax
+ dx = (xmax - x) / 2 + box[0]
+ dy = (ymax - y) / 2 + box[1]
+ self.fp.write(b"gsave\n%f %f translate\n" % (dx, dy))
+ if (x, y) != im.size:
+ # EpsImagePlugin._save prints the image at (0,0,xsize,ysize)
+ sx = x / im.size[0]
+ sy = y / im.size[1]
+ self.fp.write(b"%f %f scale\n" % (sx, sy))
+ EpsImagePlugin._save(im, self.fp, "", 0)
+ self.fp.write(b"\ngrestore\n")
+
+
+# --------------------------------------------------------------------
+# PostScript driver
+
+#
+# EDROFF.PS -- PostScript driver for Edroff 2
+#
+# History:
+# 94-01-25 fl: created (edroff 2.04)
+#
+# Copyright (c) Fredrik Lundh 1994.
+#
+
+
+EDROFF_PS = b"""\
+/S { show } bind def
+/P { moveto show } bind def
+/M { moveto } bind def
+/X { 0 rmoveto } bind def
+/Y { 0 exch rmoveto } bind def
+/E { findfont
+ dup maxlength dict begin
+ {
+ 1 index /FID ne { def } { pop pop } ifelse
+ } forall
+ /Encoding exch def
+ dup /FontName exch def
+ currentdict end definefont pop
+} bind def
+/F { findfont exch scalefont dup setfont
+ [ exch /setfont cvx ] cvx bind def
+} bind def
+"""
+
+#
+# VDI.PS -- PostScript driver for VDI meta commands
+#
+# History:
+# 94-01-25 fl: created (edroff 2.04)
+#
+# Copyright (c) Fredrik Lundh 1994.
+#
+
+VDI_PS = b"""\
+/Vm { moveto } bind def
+/Va { newpath arcn stroke } bind def
+/Vl { moveto lineto stroke } bind def
+/Vc { newpath 0 360 arc closepath } bind def
+/Vr { exch dup 0 rlineto
+ exch dup 0 exch rlineto
+ exch neg 0 rlineto
+ 0 exch neg rlineto
+ setgray fill } bind def
+/Tm matrix def
+/Ve { Tm currentmatrix pop
+ translate scale newpath 0 0 .5 0 360 arc closepath
+ Tm setmatrix
+} bind def
+/Vf { currentgray exch setgray fill setgray } bind def
+"""
+
+#
+# ERROR.PS -- Error handler
+#
+# History:
+# 89-11-21 fl: created (pslist 1.10)
+#
+
+ERROR_PS = b"""\
+/landscape false def
+/errorBUF 200 string def
+/errorNL { currentpoint 10 sub exch pop 72 exch moveto } def
+errordict begin /handleerror {
+ initmatrix /Courier findfont 10 scalefont setfont
+ newpath 72 720 moveto $error begin /newerror false def
+ (PostScript Error) show errorNL errorNL
+ (Error: ) show
+ /errorname load errorBUF cvs show errorNL errorNL
+ (Command: ) show
+ /command load dup type /stringtype ne { errorBUF cvs } if show
+ errorNL errorNL
+ (VMstatus: ) show
+ vmstatus errorBUF cvs show ( bytes available, ) show
+ errorBUF cvs show ( bytes used at level ) show
+ errorBUF cvs show errorNL errorNL
+ (Operand stargck: ) show errorNL /ostargck load {
+ dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL
+ } forall errorNL
+ (Execution stargck: ) show errorNL /estargck load {
+ dup type /stringtype ne { errorBUF cvs } if 72 0 rmoveto show errorNL
+ } forall
+ end showpage
+} def end
+"""
diff --git a/venv/lib/python3.11/site-packages/PIL/PaletteFile.py b/venv/lib/python3.11/site-packages/PIL/PaletteFile.py
new file mode 100644
index 000000000..2a26e5d4e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PaletteFile.py
@@ -0,0 +1,54 @@
+#
+# Python Imaging Library
+# $Id$
+#
+# stuff to read simple, teragon-style palette files
+#
+# History:
+# 97-08-23 fl Created
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1997.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from typing import IO
+
+from ._binary import o8
+
+
+class PaletteFile:
+ """File handler for Teragon-style palette files."""
+
+ rawmode = "RGB"
+
+ def __init__(self, fp: IO[bytes]) -> None:
+ palette = [o8(i) * 3 for i in range(256)]
+
+ while True:
+ s = fp.readline()
+
+ if not s:
+ break
+ if s.startswith(b"#"):
+ continue
+ if len(s) > 100:
+ msg = "bad palette file"
+ raise SyntaxError(msg)
+
+ v = [int(x) for x in s.split()]
+ try:
+ [i, r, g, b] = v
+ except ValueError:
+ [i, r] = v
+ g = b = r
+
+ if 0 <= i <= 255:
+ palette[i] = o8(r) + o8(g) + o8(b)
+
+ self.palette = b"".join(palette)
+
+ def getpalette(self) -> tuple[bytes, str]:
+ return self.palette, self.rawmode
diff --git a/venv/lib/python3.11/site-packages/PIL/PalmImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PalmImagePlugin.py
new file mode 100644
index 000000000..232adf3d3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PalmImagePlugin.py
@@ -0,0 +1,217 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+
+##
+# Image plugin for Palm pixmap images (output only).
+##
+from __future__ import annotations
+
+from typing import IO
+
+from . import Image, ImageFile
+from ._binary import o8
+from ._binary import o16be as o16b
+
+# fmt: off
+_Palm8BitColormapValues = (
+ (255, 255, 255), (255, 204, 255), (255, 153, 255), (255, 102, 255),
+ (255, 51, 255), (255, 0, 255), (255, 255, 204), (255, 204, 204),
+ (255, 153, 204), (255, 102, 204), (255, 51, 204), (255, 0, 204),
+ (255, 255, 153), (255, 204, 153), (255, 153, 153), (255, 102, 153),
+ (255, 51, 153), (255, 0, 153), (204, 255, 255), (204, 204, 255),
+ (204, 153, 255), (204, 102, 255), (204, 51, 255), (204, 0, 255),
+ (204, 255, 204), (204, 204, 204), (204, 153, 204), (204, 102, 204),
+ (204, 51, 204), (204, 0, 204), (204, 255, 153), (204, 204, 153),
+ (204, 153, 153), (204, 102, 153), (204, 51, 153), (204, 0, 153),
+ (153, 255, 255), (153, 204, 255), (153, 153, 255), (153, 102, 255),
+ (153, 51, 255), (153, 0, 255), (153, 255, 204), (153, 204, 204),
+ (153, 153, 204), (153, 102, 204), (153, 51, 204), (153, 0, 204),
+ (153, 255, 153), (153, 204, 153), (153, 153, 153), (153, 102, 153),
+ (153, 51, 153), (153, 0, 153), (102, 255, 255), (102, 204, 255),
+ (102, 153, 255), (102, 102, 255), (102, 51, 255), (102, 0, 255),
+ (102, 255, 204), (102, 204, 204), (102, 153, 204), (102, 102, 204),
+ (102, 51, 204), (102, 0, 204), (102, 255, 153), (102, 204, 153),
+ (102, 153, 153), (102, 102, 153), (102, 51, 153), (102, 0, 153),
+ (51, 255, 255), (51, 204, 255), (51, 153, 255), (51, 102, 255),
+ (51, 51, 255), (51, 0, 255), (51, 255, 204), (51, 204, 204),
+ (51, 153, 204), (51, 102, 204), (51, 51, 204), (51, 0, 204),
+ (51, 255, 153), (51, 204, 153), (51, 153, 153), (51, 102, 153),
+ (51, 51, 153), (51, 0, 153), (0, 255, 255), (0, 204, 255),
+ (0, 153, 255), (0, 102, 255), (0, 51, 255), (0, 0, 255),
+ (0, 255, 204), (0, 204, 204), (0, 153, 204), (0, 102, 204),
+ (0, 51, 204), (0, 0, 204), (0, 255, 153), (0, 204, 153),
+ (0, 153, 153), (0, 102, 153), (0, 51, 153), (0, 0, 153),
+ (255, 255, 102), (255, 204, 102), (255, 153, 102), (255, 102, 102),
+ (255, 51, 102), (255, 0, 102), (255, 255, 51), (255, 204, 51),
+ (255, 153, 51), (255, 102, 51), (255, 51, 51), (255, 0, 51),
+ (255, 255, 0), (255, 204, 0), (255, 153, 0), (255, 102, 0),
+ (255, 51, 0), (255, 0, 0), (204, 255, 102), (204, 204, 102),
+ (204, 153, 102), (204, 102, 102), (204, 51, 102), (204, 0, 102),
+ (204, 255, 51), (204, 204, 51), (204, 153, 51), (204, 102, 51),
+ (204, 51, 51), (204, 0, 51), (204, 255, 0), (204, 204, 0),
+ (204, 153, 0), (204, 102, 0), (204, 51, 0), (204, 0, 0),
+ (153, 255, 102), (153, 204, 102), (153, 153, 102), (153, 102, 102),
+ (153, 51, 102), (153, 0, 102), (153, 255, 51), (153, 204, 51),
+ (153, 153, 51), (153, 102, 51), (153, 51, 51), (153, 0, 51),
+ (153, 255, 0), (153, 204, 0), (153, 153, 0), (153, 102, 0),
+ (153, 51, 0), (153, 0, 0), (102, 255, 102), (102, 204, 102),
+ (102, 153, 102), (102, 102, 102), (102, 51, 102), (102, 0, 102),
+ (102, 255, 51), (102, 204, 51), (102, 153, 51), (102, 102, 51),
+ (102, 51, 51), (102, 0, 51), (102, 255, 0), (102, 204, 0),
+ (102, 153, 0), (102, 102, 0), (102, 51, 0), (102, 0, 0),
+ (51, 255, 102), (51, 204, 102), (51, 153, 102), (51, 102, 102),
+ (51, 51, 102), (51, 0, 102), (51, 255, 51), (51, 204, 51),
+ (51, 153, 51), (51, 102, 51), (51, 51, 51), (51, 0, 51),
+ (51, 255, 0), (51, 204, 0), (51, 153, 0), (51, 102, 0),
+ (51, 51, 0), (51, 0, 0), (0, 255, 102), (0, 204, 102),
+ (0, 153, 102), (0, 102, 102), (0, 51, 102), (0, 0, 102),
+ (0, 255, 51), (0, 204, 51), (0, 153, 51), (0, 102, 51),
+ (0, 51, 51), (0, 0, 51), (0, 255, 0), (0, 204, 0),
+ (0, 153, 0), (0, 102, 0), (0, 51, 0), (17, 17, 17),
+ (34, 34, 34), (68, 68, 68), (85, 85, 85), (119, 119, 119),
+ (136, 136, 136), (170, 170, 170), (187, 187, 187), (221, 221, 221),
+ (238, 238, 238), (192, 192, 192), (128, 0, 0), (128, 0, 128),
+ (0, 128, 0), (0, 128, 128), (0, 0, 0), (0, 0, 0),
+ (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0),
+ (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0),
+ (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0),
+ (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0),
+ (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0),
+ (0, 0, 0), (0, 0, 0), (0, 0, 0), (0, 0, 0))
+# fmt: on
+
+
+# so build a prototype image to be used for palette resampling
+def build_prototype_image() -> Image.Image:
+ image = Image.new("L", (1, len(_Palm8BitColormapValues)))
+ image.putdata(list(range(len(_Palm8BitColormapValues))))
+ palettedata: tuple[int, ...] = ()
+ for colormapValue in _Palm8BitColormapValues:
+ palettedata += colormapValue
+ palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues))
+ image.putpalette(palettedata)
+ return image
+
+
+Palm8BitColormapImage = build_prototype_image()
+
+# OK, we now have in Palm8BitColormapImage,
+# a "P"-mode image with the right palette
+#
+# --------------------------------------------------------------------
+
+_FLAGS = {"custom-colormap": 0x4000, "is-compressed": 0x8000, "has-transparent": 0x2000}
+
+_COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
+
+
+#
+# --------------------------------------------------------------------
+
+##
+# (Internal) Image save plugin for the Palm format.
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode == "P":
+ rawmode = "P"
+ bpp = 8
+ version = 1
+
+ elif im.mode == "L":
+ if im.encoderinfo.get("bpp") in (1, 2, 4):
+ # this is 8-bit grayscale, so we shift it to get the high-order bits,
+ # and invert it because
+ # Palm does grayscale from white (0) to black (1)
+ bpp = im.encoderinfo["bpp"]
+ maxval = (1 << bpp) - 1
+ shift = 8 - bpp
+ im = im.point(lambda x: maxval - (x >> shift))
+ elif im.info.get("bpp") in (1, 2, 4):
+ # here we assume that even though the inherent mode is 8-bit grayscale,
+ # only the lower bpp bits are significant.
+ # We invert them to match the Palm.
+ bpp = im.info["bpp"]
+ maxval = (1 << bpp) - 1
+ im = im.point(lambda x: maxval - (x & maxval))
+ else:
+ msg = f"cannot write mode {im.mode} as Palm"
+ raise OSError(msg)
+
+ # we ignore the palette here
+ im._mode = "P"
+ rawmode = f"P;{bpp}"
+ version = 1
+
+ elif im.mode == "1":
+ # monochrome -- write it inverted, as is the Palm standard
+ rawmode = "1;I"
+ bpp = 1
+ version = 0
+
+ else:
+ msg = f"cannot write mode {im.mode} as Palm"
+ raise OSError(msg)
+
+ #
+ # make sure image data is available
+ im.load()
+
+ # write header
+
+ cols = im.size[0]
+ rows = im.size[1]
+
+ rowbytes = int((cols + (16 // bpp - 1)) / (16 // bpp)) * 2
+ transparent_index = 0
+ compression_type = _COMPRESSION_TYPES["none"]
+
+ flags = 0
+ if im.mode == "P":
+ flags |= _FLAGS["custom-colormap"]
+ colormap = im.im.getpalette()
+ colors = len(colormap) // 3
+ colormapsize = 4 * colors + 2
+ else:
+ colormapsize = 0
+
+ if "offset" in im.info:
+ offset = (rowbytes * rows + 16 + 3 + colormapsize) // 4
+ else:
+ offset = 0
+
+ fp.write(o16b(cols) + o16b(rows) + o16b(rowbytes) + o16b(flags))
+ fp.write(o8(bpp))
+ fp.write(o8(version))
+ fp.write(o16b(offset))
+ fp.write(o8(transparent_index))
+ fp.write(o8(compression_type))
+ fp.write(o16b(0)) # reserved by Palm
+
+ # now write colormap if necessary
+
+ if colormapsize:
+ fp.write(o16b(colors))
+ for i in range(colors):
+ fp.write(o8(i))
+ fp.write(colormap[3 * i : 3 * i + 3])
+
+ # now convert data to raw form
+ ImageFile._save(
+ im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]
+ )
+
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+#
+# --------------------------------------------------------------------
+
+Image.register_save("PALM", _save)
+
+Image.register_extension("PALM", ".palm")
+
+Image.register_mime("PALM", "image/palm")
diff --git a/venv/lib/python3.11/site-packages/PIL/PcdImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PcdImagePlugin.py
new file mode 100644
index 000000000..296f3775b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PcdImagePlugin.py
@@ -0,0 +1,68 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# PCD file handling
+#
+# History:
+# 96-05-10 fl Created
+# 96-05-27 fl Added draft mode (128x192, 256x384)
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1996.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from . import Image, ImageFile
+
+##
+# Image plugin for PhotoCD images. This plugin only reads the 768x512
+# image from the file; higher resolutions are encoded in a proprietary
+# encoding.
+
+
+class PcdImageFile(ImageFile.ImageFile):
+ format = "PCD"
+ format_description = "Kodak PhotoCD"
+
+ def _open(self) -> None:
+ # rough
+ assert self.fp is not None
+
+ self.fp.seek(2048)
+ s = self.fp.read(1539)
+
+ if not s.startswith(b"PCD_"):
+ msg = "not a PCD file"
+ raise SyntaxError(msg)
+
+ orientation = s[1538] & 3
+ self.tile_post_rotate = None
+ if orientation == 1:
+ self.tile_post_rotate = 90
+ elif orientation == 3:
+ self.tile_post_rotate = 270
+
+ self._mode = "RGB"
+ self._size = (512, 768) if orientation in (1, 3) else (768, 512)
+ self.tile = [ImageFile._Tile("pcd", (0, 0, 768, 512), 96 * 2048)]
+
+ def load_prepare(self) -> None:
+ if self._im is None and self.tile_post_rotate:
+ self.im = Image.core.new(self.mode, (768, 512))
+ ImageFile.ImageFile.load_prepare(self)
+
+ def load_end(self) -> None:
+ if self.tile_post_rotate:
+ # Handle rotated PCDs
+ self.im = self.rotate(self.tile_post_rotate, expand=True).im
+
+
+#
+# registry
+
+Image.register_open(PcdImageFile.format, PcdImageFile)
+
+Image.register_extension(PcdImageFile.format, ".pcd")
diff --git a/venv/lib/python3.11/site-packages/PIL/PcfFontFile.py b/venv/lib/python3.11/site-packages/PIL/PcfFontFile.py
new file mode 100644
index 000000000..985444106
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PcfFontFile.py
@@ -0,0 +1,256 @@
+#
+# THIS IS WORK IN PROGRESS
+#
+# The Python Imaging Library
+# $Id$
+#
+# portable compiled font file parser
+#
+# history:
+# 1997-08-19 fl created
+# 2003-09-13 fl fixed loading of unicode fonts
+#
+# Copyright (c) 1997-2003 by Secret Labs AB.
+# Copyright (c) 1997-2003 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+
+from . import FontFile, Image
+from ._binary import i8
+from ._binary import i16be as b16
+from ._binary import i16le as l16
+from ._binary import i32be as b32
+from ._binary import i32le as l32
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import BinaryIO
+
+# --------------------------------------------------------------------
+# declarations
+
+PCF_MAGIC = 0x70636601 # "\x01fcp"
+
+PCF_PROPERTIES = 1 << 0
+PCF_ACCELERATORS = 1 << 1
+PCF_METRICS = 1 << 2
+PCF_BITMAPS = 1 << 3
+PCF_INK_METRICS = 1 << 4
+PCF_BDF_ENCODINGS = 1 << 5
+PCF_SWIDTHS = 1 << 6
+PCF_GLYPH_NAMES = 1 << 7
+PCF_BDF_ACCELERATORS = 1 << 8
+
+BYTES_PER_ROW: list[Callable[[int], int]] = [
+ lambda bits: ((bits + 7) >> 3),
+ lambda bits: ((bits + 15) >> 3) & ~1,
+ lambda bits: ((bits + 31) >> 3) & ~3,
+ lambda bits: ((bits + 63) >> 3) & ~7,
+]
+
+
+def sz(s: bytes, o: int) -> bytes:
+ return s[o : s.index(b"\0", o)]
+
+
+class PcfFontFile(FontFile.FontFile):
+ """Font file plugin for the X11 PCF format."""
+
+ name = "name"
+
+ def __init__(self, fp: BinaryIO, charset_encoding: str = "iso8859-1"):
+ self.charset_encoding = charset_encoding
+
+ magic = l32(fp.read(4))
+ if magic != PCF_MAGIC:
+ msg = "not a PCF file"
+ raise SyntaxError(msg)
+
+ super().__init__()
+
+ count = l32(fp.read(4))
+ self.toc = {}
+ for i in range(count):
+ type = l32(fp.read(4))
+ self.toc[type] = l32(fp.read(4)), l32(fp.read(4)), l32(fp.read(4))
+
+ self.fp = fp
+
+ self.info = self._load_properties()
+
+ metrics = self._load_metrics()
+ bitmaps = self._load_bitmaps(metrics)
+ encoding = self._load_encoding()
+
+ #
+ # create glyph structure
+
+ for ch, ix in enumerate(encoding):
+ if ix is not None:
+ (
+ xsize,
+ ysize,
+ left,
+ right,
+ width,
+ ascent,
+ descent,
+ attributes,
+ ) = metrics[ix]
+ self.glyph[ch] = (
+ (width, 0),
+ (left, descent - ysize, xsize + left, descent),
+ (0, 0, xsize, ysize),
+ bitmaps[ix],
+ )
+
+ def _getformat(
+ self, tag: int
+ ) -> tuple[BinaryIO, int, Callable[[bytes], int], Callable[[bytes], int]]:
+ format, size, offset = self.toc[tag]
+
+ fp = self.fp
+ fp.seek(offset)
+
+ format = l32(fp.read(4))
+
+ if format & 4:
+ i16, i32 = b16, b32
+ else:
+ i16, i32 = l16, l32
+
+ return fp, format, i16, i32
+
+ def _load_properties(self) -> dict[bytes, bytes | int]:
+ #
+ # font properties
+
+ properties = {}
+
+ fp, format, i16, i32 = self._getformat(PCF_PROPERTIES)
+
+ nprops = i32(fp.read(4))
+
+ # read property description
+ p = [(i32(fp.read(4)), i8(fp.read(1)), i32(fp.read(4))) for _ in range(nprops)]
+
+ if nprops & 3:
+ fp.seek(4 - (nprops & 3), io.SEEK_CUR) # pad
+
+ data = fp.read(i32(fp.read(4)))
+
+ for k, s, v in p:
+ property_value: bytes | int = sz(data, v) if s else v
+ properties[sz(data, k)] = property_value
+
+ return properties
+
+ def _load_metrics(self) -> list[tuple[int, int, int, int, int, int, int, int]]:
+ #
+ # font metrics
+
+ metrics: list[tuple[int, int, int, int, int, int, int, int]] = []
+
+ fp, format, i16, i32 = self._getformat(PCF_METRICS)
+
+ def append(
+ left: int,
+ right: int,
+ width: int,
+ ascent: int,
+ descent: int,
+ attributes: int = 0,
+ ) -> None:
+ xsize = right - left
+ ysize = ascent + descent
+ Image._decompression_bomb_check((xsize, ysize))
+ metrics.append(
+ (xsize, ysize, left, right, width, ascent, descent, attributes)
+ )
+
+ if (format & 0xFF00) == 0x100:
+ # "compressed" metrics
+ for i in range(i16(fp.read(2))):
+ append(*(i8(fp.read(1)) - 128 for _ in range(5)))
+
+ else:
+ # "jumbo" metrics
+ for i in range(i32(fp.read(4))):
+ append(*(i16(fp.read(2)) for _ in range(6)))
+
+ return metrics
+
+ def _load_bitmaps(
+ self, metrics: list[tuple[int, int, int, int, int, int, int, int]]
+ ) -> list[Image.Image]:
+ #
+ # bitmap data
+
+ fp, format, i16, i32 = self._getformat(PCF_BITMAPS)
+
+ nbitmaps = i32(fp.read(4))
+
+ if nbitmaps != len(metrics):
+ msg = "Wrong number of bitmaps"
+ raise OSError(msg)
+
+ offsets = [i32(fp.read(4)) for _ in range(nbitmaps)]
+
+ bitmap_sizes = [i32(fp.read(4)) for _ in range(4)]
+
+ # byteorder = format & 4 # non-zero => MSB
+ bitorder = format & 8 # non-zero => MSB
+ padindex = format & 3
+
+ bitmapsize = bitmap_sizes[padindex]
+ offsets.append(bitmapsize)
+
+ data = fp.read(bitmapsize)
+
+ pad = BYTES_PER_ROW[padindex]
+ mode = "1;R"
+ if bitorder:
+ mode = "1"
+
+ bitmaps = []
+ for i in range(nbitmaps):
+ xsize, ysize = metrics[i][:2]
+ b, e = offsets[i : i + 2]
+ bitmaps.append(
+ Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize))
+ )
+
+ return bitmaps
+
+ def _load_encoding(self) -> list[int | None]:
+ fp, format, i16, i32 = self._getformat(PCF_BDF_ENCODINGS)
+
+ first_col, last_col = i16(fp.read(2)), i16(fp.read(2))
+ first_row, last_row = i16(fp.read(2)), i16(fp.read(2))
+
+ i16(fp.read(2)) # default
+
+ nencoding = (last_col - first_col + 1) * (last_row - first_row + 1)
+
+ # map character code to bitmap index
+ encoding: list[int | None] = [None] * min(256, nencoding)
+
+ encoding_offsets = [i16(fp.read(2)) for _ in range(nencoding)]
+
+ for i in range(first_col, len(encoding)):
+ try:
+ encoding_offset = encoding_offsets[
+ ord(bytearray([i]).decode(self.charset_encoding))
+ ]
+ if encoding_offset != 0xFFFF:
+ encoding[i] = encoding_offset
+ except UnicodeDecodeError: # noqa: PERF203
+ # character is not supported in selected encoding
+ pass
+
+ return encoding
diff --git a/venv/lib/python3.11/site-packages/PIL/PcxImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PcxImagePlugin.py
new file mode 100644
index 000000000..3e34e3c63
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PcxImagePlugin.py
@@ -0,0 +1,232 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# PCX file handling
+#
+# This format was originally used by ZSoft's popular PaintBrush
+# program for the IBM PC. It is also supported by many MS-DOS and
+# Windows applications, including the Windows PaintBrush program in
+# Windows 3.
+#
+# history:
+# 1995-09-01 fl Created
+# 1996-05-20 fl Fixed RGB support
+# 1997-01-03 fl Fixed 2-bit and 4-bit support
+# 1999-02-03 fl Fixed 8-bit support (broken in 1.0b1)
+# 1999-02-07 fl Added write support
+# 2002-06-09 fl Made 2-bit and 4-bit support a bit more robust
+# 2002-07-30 fl Seek from to current position, not beginning of file
+# 2003-06-03 fl Extract DPI settings (info["dpi"])
+#
+# Copyright (c) 1997-2003 by Secret Labs AB.
+# Copyright (c) 1995-2003 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+import logging
+from typing import IO
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import i16le as i16
+from ._binary import o8
+from ._binary import o16le as o16
+
+logger = logging.getLogger(__name__)
+
+
+def _accept(prefix: bytes) -> bool:
+ return len(prefix) >= 2 and prefix[0] == 10 and prefix[1] in [0, 2, 3, 5]
+
+
+##
+# Image plugin for Paintbrush images.
+
+
+class PcxImageFile(ImageFile.ImageFile):
+ format = "PCX"
+ format_description = "Paintbrush"
+
+ def _open(self) -> None:
+ # header
+ assert self.fp is not None
+
+ s = self.fp.read(68)
+ if not _accept(s):
+ msg = "not a PCX file"
+ raise SyntaxError(msg)
+
+ # image
+ bbox = i16(s, 4), i16(s, 6), i16(s, 8) + 1, i16(s, 10) + 1
+ if bbox[2] <= bbox[0] or bbox[3] <= bbox[1]:
+ msg = "bad PCX image size"
+ raise SyntaxError(msg)
+ logger.debug("BBox: %s %s %s %s", *bbox)
+
+ offset = self.fp.tell() + 60
+
+ # format
+ version = s[1]
+ bits = s[3]
+ planes = s[65]
+ provided_stride = i16(s, 66)
+ logger.debug(
+ "PCX version %s, bits %s, planes %s, stride %s",
+ version,
+ bits,
+ planes,
+ provided_stride,
+ )
+
+ self.info["dpi"] = i16(s, 12), i16(s, 14)
+
+ if bits == 1 and planes == 1:
+ mode = rawmode = "1"
+
+ elif bits == 1 and planes in (2, 4):
+ mode = "P"
+ rawmode = f"P;{planes}L"
+ self.palette = ImagePalette.raw("RGB", s[16:64])
+
+ elif version == 5 and bits == 8 and planes == 1:
+ mode = rawmode = "L"
+ # FIXME: hey, this doesn't work with the incremental loader !!!
+ self.fp.seek(-769, io.SEEK_END)
+ s = self.fp.read(769)
+ if len(s) == 769 and s[0] == 12:
+ # check if the palette is linear grayscale
+ for i in range(256):
+ if s[i * 3 + 1 : i * 3 + 4] != o8(i) * 3:
+ mode = rawmode = "P"
+ break
+ if mode == "P":
+ self.palette = ImagePalette.raw("RGB", s[1:])
+
+ elif version == 5 and bits == 8 and planes == 3:
+ mode = "RGB"
+ rawmode = "RGB;L"
+
+ else:
+ msg = "unknown PCX mode"
+ raise OSError(msg)
+
+ self._mode = mode
+ self._size = bbox[2] - bbox[0], bbox[3] - bbox[1]
+
+ # Don't trust the passed in stride.
+ # Calculate the approximate position for ourselves.
+ # CVE-2020-35653
+ stride = (self._size[0] * bits + 7) // 8
+
+ # While the specification states that this must be even,
+ # not all images follow this
+ if provided_stride != stride:
+ stride += stride % 2
+
+ bbox = (0, 0) + self.size
+ logger.debug("size: %sx%s", *self.size)
+
+ self.tile = [ImageFile._Tile("pcx", bbox, offset, (rawmode, planes * stride))]
+
+
+# --------------------------------------------------------------------
+# save PCX files
+
+
+SAVE = {
+ # mode: (version, bits, planes, raw mode)
+ "1": (2, 1, 1, "1"),
+ "L": (5, 8, 1, "L"),
+ "P": (5, 8, 1, "P"),
+ "RGB": (5, 8, 3, "RGB;L"),
+}
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.width == 0 or im.height == 0:
+ msg = "Cannot write empty image as PCX"
+ raise ValueError(msg)
+
+ try:
+ version, bits, planes, rawmode = SAVE[im.mode]
+ except KeyError as e:
+ msg = f"Cannot save {im.mode} images as PCX"
+ raise ValueError(msg) from e
+
+ # bytes per plane
+ stride = (im.size[0] * bits + 7) // 8
+ # stride should be even
+ stride += stride % 2
+ # Stride needs to be kept in sync with the PcxEncode.c version.
+ # Ideally it should be passed in in the state, but the bytes value
+ # gets overwritten.
+
+ logger.debug(
+ "PcxImagePlugin._save: xwidth: %d, bits: %d, stride: %d",
+ im.size[0],
+ bits,
+ stride,
+ )
+
+ # under windows, we could determine the current screen size with
+ # "Image.core.display_mode()[1]", but I think that's overkill...
+
+ screen = im.size
+
+ dpi = 100, 100
+
+ # PCX header
+ fp.write(
+ o8(10)
+ + o8(version)
+ + o8(1)
+ + o8(bits)
+ + o16(0)
+ + o16(0)
+ + o16(im.size[0] - 1)
+ + o16(im.size[1] - 1)
+ + o16(dpi[0])
+ + o16(dpi[1])
+ + b"\0" * 24
+ + b"\xff" * 24
+ + b"\0"
+ + o8(planes)
+ + o16(stride)
+ + o16(1)
+ + o16(screen[0])
+ + o16(screen[1])
+ + b"\0" * 54
+ )
+
+ assert fp.tell() == 128
+
+ ImageFile._save(
+ im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]
+ )
+
+ if im.mode == "P":
+ # colour palette
+ fp.write(o8(12))
+ palette = im.im.getpalette("RGB", "RGB")
+ palette += b"\x00" * (768 - len(palette))
+ fp.write(palette) # 768 bytes
+ elif im.mode == "L":
+ # grayscale palette
+ fp.write(o8(12))
+ for i in range(256):
+ fp.write(o8(i) * 3)
+
+
+# --------------------------------------------------------------------
+# registry
+
+
+Image.register_open(PcxImageFile.format, PcxImageFile, _accept)
+Image.register_save(PcxImageFile.format, _save)
+
+Image.register_extension(PcxImageFile.format, ".pcx")
+
+Image.register_mime(PcxImageFile.format, "image/x-pcx")
diff --git a/venv/lib/python3.11/site-packages/PIL/PdfImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PdfImagePlugin.py
new file mode 100644
index 000000000..cb26786b0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PdfImagePlugin.py
@@ -0,0 +1,315 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# PDF (Acrobat) file handling
+#
+# History:
+# 1996-07-16 fl Created
+# 1997-01-18 fl Fixed header
+# 2004-02-21 fl Fixes for 1/L/CMYK images, etc.
+# 2004-02-24 fl Fixes for 1 and P images.
+#
+# Copyright (c) 1997-2004 by Secret Labs AB. All rights reserved.
+# Copyright (c) 1996-1997 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+##
+# Image plugin for PDF images (output only).
+##
+from __future__ import annotations
+
+import io
+import math
+import os
+import time
+from typing import IO, Any
+
+from . import Image, ImageFile, ImageSequence, PdfParser, features
+
+#
+# --------------------------------------------------------------------
+
+# object ids:
+# 1. catalogue
+# 2. pages
+# 3. image
+# 4. page
+# 5. page contents
+
+
+def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ _save(im, fp, filename, save_all=True)
+
+
+##
+# (Internal) Image save plugin for the PDF format.
+
+
+def _write_image(
+ im: Image.Image,
+ filename: str | bytes,
+ existing_pdf: PdfParser.PdfParser,
+ image_refs: list[PdfParser.IndirectReference],
+) -> tuple[PdfParser.IndirectReference, str]:
+ # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
+ # (packbits) or LZWDecode (tiff/lzw compression). Note that
+ # PDF 1.2 also supports Flatedecode (zip compression).
+
+ params = None
+ decode = None
+
+ #
+ # Get image characteristics
+
+ width, height = im.size
+
+ dict_obj: dict[str, Any] = {"BitsPerComponent": 8}
+ if im.mode == "1":
+ if features.check("libtiff"):
+ decode_filter = "CCITTFaxDecode"
+ dict_obj["BitsPerComponent"] = 1
+ params = PdfParser.PdfArray(
+ [
+ PdfParser.PdfDict(
+ {
+ "K": -1,
+ "BlackIs1": True,
+ "Columns": width,
+ "Rows": height,
+ }
+ )
+ ]
+ )
+ else:
+ decode_filter = "DCTDecode"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
+ procset = "ImageB" # grayscale
+ elif im.mode == "L":
+ decode_filter = "DCTDecode"
+ # params = f"<< /Predictor 15 /Columns {width-2} >>"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
+ procset = "ImageB" # grayscale
+ elif im.mode == "LA":
+ decode_filter = "JPXDecode"
+ # params = f"<< /Predictor 15 /Columns {width-2} >>"
+ procset = "ImageB" # grayscale
+ dict_obj["SMaskInData"] = 1
+ elif im.mode == "P":
+ decode_filter = "ASCIIHexDecode"
+ palette = im.getpalette()
+ assert palette is not None
+ dict_obj["ColorSpace"] = [
+ PdfParser.PdfName("Indexed"),
+ PdfParser.PdfName("DeviceRGB"),
+ len(palette) // 3 - 1,
+ PdfParser.PdfBinary(palette),
+ ]
+ procset = "ImageI" # indexed color
+
+ if "transparency" in im.info:
+ smask = im.convert("LA").getchannel("A")
+ smask.encoderinfo = {}
+
+ image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0]
+ dict_obj["SMask"] = image_ref
+ elif im.mode == "RGB":
+ decode_filter = "DCTDecode"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
+ procset = "ImageC" # color images
+ elif im.mode == "RGBA":
+ decode_filter = "JPXDecode"
+ procset = "ImageC" # color images
+ dict_obj["SMaskInData"] = 1
+ elif im.mode == "CMYK":
+ decode_filter = "DCTDecode"
+ dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
+ procset = "ImageC" # color images
+ decode = [1, 0, 1, 0, 1, 0, 1, 0]
+ else:
+ msg = f"cannot save mode {im.mode}"
+ raise ValueError(msg)
+
+ #
+ # image
+
+ op = io.BytesIO()
+
+ if decode_filter == "ASCIIHexDecode":
+ ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)])
+ elif decode_filter == "CCITTFaxDecode":
+ im.save(
+ op,
+ "TIFF",
+ compression="group4",
+ # use a single strip
+ strip_size=math.ceil(width / 8) * height,
+ )
+ elif decode_filter == "DCTDecode":
+ from . import JpegImagePlugin
+
+ JpegImagePlugin._save(im, op, filename)
+ elif decode_filter == "JPXDecode":
+ from . import Jpeg2KImagePlugin
+
+ del dict_obj["BitsPerComponent"]
+ Jpeg2KImagePlugin._save(im, op, filename)
+ else:
+ msg = f"unsupported PDF filter ({decode_filter})"
+ raise ValueError(msg)
+
+ stream = op.getvalue()
+ filter: PdfParser.PdfArray | PdfParser.PdfName
+ if decode_filter == "CCITTFaxDecode":
+ stream = stream[8:]
+ filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)])
+ else:
+ filter = PdfParser.PdfName(decode_filter)
+
+ image_ref = image_refs.pop(0)
+ existing_pdf.write_obj(
+ image_ref,
+ stream=stream,
+ Type=PdfParser.PdfName("XObject"),
+ Subtype=PdfParser.PdfName("Image"),
+ Width=width, # * 72.0 / x_resolution,
+ Height=height, # * 72.0 / y_resolution,
+ Filter=filter,
+ Decode=decode,
+ DecodeParms=params,
+ **dict_obj,
+ )
+
+ return image_ref, procset
+
+
+def _save(
+ im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
+) -> None:
+ is_appending = im.encoderinfo.get("append", False)
+ filename_str = filename.decode() if isinstance(filename, bytes) else filename
+ if is_appending:
+ existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="r+b")
+ else:
+ existing_pdf = PdfParser.PdfParser(f=fp, filename=filename_str, mode="w+b")
+
+ dpi = im.encoderinfo.get("dpi")
+ if dpi:
+ x_resolution = dpi[0]
+ y_resolution = dpi[1]
+ else:
+ x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0)
+
+ info = {
+ "title": (
+ None if is_appending else os.path.splitext(os.path.basename(filename))[0]
+ ),
+ "author": None,
+ "subject": None,
+ "keywords": None,
+ "creator": None,
+ "producer": None,
+ "creationDate": None if is_appending else time.gmtime(),
+ "modDate": None if is_appending else time.gmtime(),
+ }
+ for k, default in info.items():
+ v = im.encoderinfo.get(k) if k in im.encoderinfo else default
+ if v:
+ existing_pdf.info[k[0].upper() + k[1:]] = v
+
+ #
+ # make sure image data is available
+ im.load()
+
+ existing_pdf.start_writing()
+ existing_pdf.write_header()
+ existing_pdf.write_comment("created by Pillow PDF driver")
+
+ #
+ # pages
+ ims = [im]
+ if save_all:
+ append_images = im.encoderinfo.get("append_images", [])
+ for append_im in append_images:
+ append_im.encoderinfo = im.encoderinfo.copy()
+ ims.append(append_im)
+ number_of_pages = 0
+ image_refs = []
+ page_refs = []
+ contents_refs = []
+ for im in ims:
+ im_number_of_pages = 1
+ if save_all:
+ im_number_of_pages = getattr(im, "n_frames", 1)
+ number_of_pages += im_number_of_pages
+ for i in range(im_number_of_pages):
+ image_refs.append(existing_pdf.next_object_id(0))
+ if im.mode == "P" and "transparency" in im.info:
+ image_refs.append(existing_pdf.next_object_id(0))
+
+ page_refs.append(existing_pdf.next_object_id(0))
+ contents_refs.append(existing_pdf.next_object_id(0))
+ existing_pdf.pages.append(page_refs[-1])
+
+ #
+ # catalog and list of pages
+ existing_pdf.write_catalog()
+
+ page_number = 0
+ for im_sequence in ims:
+ im_pages: ImageSequence.Iterator | list[Image.Image] = (
+ ImageSequence.Iterator(im_sequence) if save_all else [im_sequence]
+ )
+ for im in im_pages:
+ image_ref, procset = _write_image(im, filename, existing_pdf, image_refs)
+
+ #
+ # page
+
+ existing_pdf.write_page(
+ page_refs[page_number],
+ Resources=PdfParser.PdfDict(
+ ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)],
+ XObject=PdfParser.PdfDict(image=image_ref),
+ ),
+ MediaBox=[
+ 0,
+ 0,
+ im.width * 72.0 / x_resolution,
+ im.height * 72.0 / y_resolution,
+ ],
+ Contents=contents_refs[page_number],
+ )
+
+ #
+ # page contents
+
+ page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % (
+ im.width * 72.0 / x_resolution,
+ im.height * 72.0 / y_resolution,
+ )
+
+ existing_pdf.write_obj(contents_refs[page_number], stream=page_contents)
+
+ page_number += 1
+
+ #
+ # trailer
+ existing_pdf.write_xref_and_trailer()
+ if hasattr(fp, "flush"):
+ fp.flush()
+ existing_pdf.close()
+
+
+#
+# --------------------------------------------------------------------
+
+
+Image.register_save("PDF", _save)
+Image.register_save_all("PDF", _save_all)
+
+Image.register_extension("PDF", ".pdf")
+
+Image.register_mime("PDF", "application/pdf")
diff --git a/venv/lib/python3.11/site-packages/PIL/PdfParser.py b/venv/lib/python3.11/site-packages/PIL/PdfParser.py
new file mode 100644
index 000000000..c2e1568f9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PdfParser.py
@@ -0,0 +1,1092 @@
+from __future__ import annotations
+
+import calendar
+import codecs
+import collections
+import mmap
+import os
+import re
+import time
+import zlib
+from typing import Any, NamedTuple
+
+from . import ImageFile
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import IO
+
+ _DictBase = collections.UserDict[str | bytes, Any]
+else:
+ _DictBase = collections.UserDict
+
+
+# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
+# on page 656
+def encode_text(s: str) -> bytes:
+ return codecs.BOM_UTF16_BE + s.encode("utf_16_be")
+
+
+PDFDocEncoding = {
+ 0x16: "\u0017",
+ 0x18: "\u02d8",
+ 0x19: "\u02c7",
+ 0x1A: "\u02c6",
+ 0x1B: "\u02d9",
+ 0x1C: "\u02dd",
+ 0x1D: "\u02db",
+ 0x1E: "\u02da",
+ 0x1F: "\u02dc",
+ 0x80: "\u2022",
+ 0x81: "\u2020",
+ 0x82: "\u2021",
+ 0x83: "\u2026",
+ 0x84: "\u2014",
+ 0x85: "\u2013",
+ 0x86: "\u0192",
+ 0x87: "\u2044",
+ 0x88: "\u2039",
+ 0x89: "\u203a",
+ 0x8A: "\u2212",
+ 0x8B: "\u2030",
+ 0x8C: "\u201e",
+ 0x8D: "\u201c",
+ 0x8E: "\u201d",
+ 0x8F: "\u2018",
+ 0x90: "\u2019",
+ 0x91: "\u201a",
+ 0x92: "\u2122",
+ 0x93: "\ufb01",
+ 0x94: "\ufb02",
+ 0x95: "\u0141",
+ 0x96: "\u0152",
+ 0x97: "\u0160",
+ 0x98: "\u0178",
+ 0x99: "\u017d",
+ 0x9A: "\u0131",
+ 0x9B: "\u0142",
+ 0x9C: "\u0153",
+ 0x9D: "\u0161",
+ 0x9E: "\u017e",
+ 0xA0: "\u20ac",
+}
+
+
+def decode_text(b: bytes) -> str:
+ if b[: len(codecs.BOM_UTF16_BE)] == codecs.BOM_UTF16_BE:
+ return b[len(codecs.BOM_UTF16_BE) :].decode("utf_16_be")
+ else:
+ return "".join(PDFDocEncoding.get(byte, chr(byte)) for byte in b)
+
+
+class PdfFormatError(RuntimeError):
+ """An error that probably indicates a syntactic or semantic error in the
+ PDF file structure"""
+
+ pass
+
+
+def check_format_condition(condition: bool, error_message: str) -> None:
+ if not condition:
+ raise PdfFormatError(error_message)
+
+
+class IndirectReferenceTuple(NamedTuple):
+ object_id: int
+ generation: int
+
+
+class IndirectReference(IndirectReferenceTuple):
+ def __str__(self) -> str:
+ return f"{self.object_id} {self.generation} R"
+
+ def __bytes__(self) -> bytes:
+ return self.__str__().encode("us-ascii")
+
+ def __eq__(self, other: object) -> bool:
+ if self.__class__ is not other.__class__:
+ return False
+ assert isinstance(other, IndirectReference)
+ return other.object_id == self.object_id and other.generation == self.generation
+
+ def __ne__(self, other: object) -> bool:
+ return not (self == other)
+
+ def __hash__(self) -> int:
+ return hash((self.object_id, self.generation))
+
+
+class IndirectObjectDef(IndirectReference):
+ def __str__(self) -> str:
+ return f"{self.object_id} {self.generation} obj"
+
+
+class XrefTable:
+ def __init__(self) -> None:
+ self.existing_entries: dict[int, tuple[int, int]] = (
+ {}
+ ) # object ID => (offset, generation)
+ self.new_entries: dict[int, tuple[int, int]] = (
+ {}
+ ) # object ID => (offset, generation)
+ self.deleted_entries = {0: 65536} # object ID => generation
+ self.reading_finished = False
+
+ def __setitem__(self, key: int, value: tuple[int, int]) -> None:
+ if self.reading_finished:
+ self.new_entries[key] = value
+ else:
+ self.existing_entries[key] = value
+ if key in self.deleted_entries:
+ del self.deleted_entries[key]
+
+ def __getitem__(self, key: int) -> tuple[int, int]:
+ try:
+ return self.new_entries[key]
+ except KeyError:
+ return self.existing_entries[key]
+
+ def __delitem__(self, key: int) -> None:
+ if key in self.new_entries:
+ generation = self.new_entries[key][1] + 1
+ del self.new_entries[key]
+ self.deleted_entries[key] = generation
+ elif key in self.existing_entries:
+ generation = self.existing_entries[key][1] + 1
+ self.deleted_entries[key] = generation
+ elif key in self.deleted_entries:
+ generation = self.deleted_entries[key]
+ else:
+ msg = f"object ID {key} cannot be deleted because it doesn't exist"
+ raise IndexError(msg)
+
+ def __contains__(self, key: int) -> bool:
+ return key in self.existing_entries or key in self.new_entries
+
+ def __len__(self) -> int:
+ return len(
+ set(self.existing_entries.keys())
+ | set(self.new_entries.keys())
+ | set(self.deleted_entries.keys())
+ )
+
+ def keys(self) -> set[int]:
+ return (
+ set(self.existing_entries.keys()) - set(self.deleted_entries.keys())
+ ) | set(self.new_entries.keys())
+
+ def write(self, f: IO[bytes]) -> int:
+ keys = sorted(set(self.new_entries.keys()) | set(self.deleted_entries.keys()))
+ deleted_keys = sorted(set(self.deleted_entries.keys()))
+ startxref = f.tell()
+ f.write(b"xref\n")
+ while keys:
+ # find a contiguous sequence of object IDs
+ prev: int | None = None
+ for index, key in enumerate(keys):
+ if prev is None or prev + 1 == key:
+ prev = key
+ else:
+ contiguous_keys = keys[:index]
+ keys = keys[index:]
+ break
+ else:
+ contiguous_keys = keys
+ keys = []
+ f.write(b"%d %d\n" % (contiguous_keys[0], len(contiguous_keys)))
+ for object_id in contiguous_keys:
+ if object_id in self.new_entries:
+ f.write(b"%010d %05d n \n" % self.new_entries[object_id])
+ else:
+ this_deleted_object_id = deleted_keys.pop(0)
+ check_format_condition(
+ object_id == this_deleted_object_id,
+ f"expected the next deleted object ID to be {object_id}, "
+ f"instead found {this_deleted_object_id}",
+ )
+ try:
+ next_in_linked_list = deleted_keys[0]
+ except IndexError:
+ next_in_linked_list = 0
+ f.write(
+ b"%010d %05d f \n"
+ % (next_in_linked_list, self.deleted_entries[object_id])
+ )
+ return startxref
+
+
+class PdfName:
+ name: bytes
+
+ def __init__(self, name: PdfName | bytes | str) -> None:
+ if isinstance(name, PdfName):
+ self.name = name.name
+ elif isinstance(name, bytes):
+ self.name = name
+ else:
+ self.name = name.encode("us-ascii")
+
+ def name_as_str(self) -> str:
+ return self.name.decode("us-ascii")
+
+ def __eq__(self, other: object) -> bool:
+ return (
+ isinstance(other, PdfName) and other.name == self.name
+ ) or other == self.name
+
+ def __hash__(self) -> int:
+ return hash(self.name)
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({repr(self.name)})"
+
+ @classmethod
+ def from_pdf_stream(cls, data: bytes) -> PdfName:
+ return cls(PdfParser.interpret_name(data))
+
+ allowed_chars = set(range(33, 127)) - {ord(c) for c in "#%/()<>[]{}"}
+
+ def __bytes__(self) -> bytes:
+ result = bytearray(b"/")
+ for b in self.name:
+ if b in self.allowed_chars:
+ result.append(b)
+ else:
+ result.extend(b"#%02X" % b)
+ return bytes(result)
+
+
+class PdfArray(list[Any]):
+ def __bytes__(self) -> bytes:
+ return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"
+
+
+class PdfDict(_DictBase):
+ def __setattr__(self, key: str, value: Any) -> None:
+ if key == "data":
+ collections.UserDict.__setattr__(self, key, value)
+ else:
+ self[key.encode("us-ascii")] = value
+
+ def __getattr__(self, key: str) -> str | time.struct_time:
+ try:
+ value = self[key.encode("us-ascii")]
+ except KeyError as e:
+ raise AttributeError(key) from e
+ if isinstance(value, bytes):
+ value = decode_text(value)
+ if key.endswith("Date"):
+ if value.startswith("D:"):
+ value = value[2:]
+
+ relationship = "Z"
+ if len(value) > 17:
+ relationship = value[14]
+ offset = int(value[15:17]) * 60
+ if len(value) > 20:
+ offset += int(value[18:20])
+
+ format = "%Y%m%d%H%M%S"[: len(value) - 2]
+ value = time.strptime(value[: len(format) + 2], format)
+ if relationship in ["+", "-"]:
+ offset *= 60
+ if relationship == "+":
+ offset *= -1
+ value = time.gmtime(calendar.timegm(value) + offset)
+ return value
+
+ def __bytes__(self) -> bytes:
+ out = bytearray(b"<<")
+ for key, value in self.items():
+ if value is None:
+ continue
+ value = pdf_repr(value)
+ out.extend(b"\n")
+ out.extend(bytes(PdfName(key)))
+ out.extend(b" ")
+ out.extend(value)
+ out.extend(b"\n>>")
+ return bytes(out)
+
+
+class PdfBinary:
+ def __init__(self, data: list[int] | bytes) -> None:
+ self.data = data
+
+ def __bytes__(self) -> bytes:
+ return b"<%s>" % b"".join(b"%02X" % b for b in self.data)
+
+
+class PdfStream:
+ def __init__(self, dictionary: PdfDict, buf: bytes) -> None:
+ self.dictionary = dictionary
+ self.buf = buf
+
+ def decode(self, max_length: int = ImageFile.SAFEBLOCK) -> bytes:
+ try:
+ filter = self.dictionary[b"Filter"]
+ except KeyError:
+ return self.buf
+ if filter == b"FlateDecode":
+ dobj = zlib.decompressobj()
+ plaintext = dobj.decompress(self.buf, max_length)
+ if dobj.unconsumed_tail:
+ msg = "Decompressed data too large"
+ raise ValueError(msg)
+ return plaintext
+ else:
+ msg = f"stream filter {repr(filter)} unknown/unsupported"
+ raise NotImplementedError(msg)
+
+
+def pdf_repr(x: Any) -> bytes:
+ if x is True:
+ return b"true"
+ elif x is False:
+ return b"false"
+ elif x is None:
+ return b"null"
+ elif isinstance(x, (PdfName, PdfDict, PdfArray, PdfBinary)):
+ return bytes(x)
+ elif isinstance(x, (int, float)):
+ return str(x).encode("us-ascii")
+ elif isinstance(x, time.struct_time):
+ return b"(D:" + time.strftime("%Y%m%d%H%M%SZ", x).encode("us-ascii") + b")"
+ elif isinstance(x, dict):
+ return bytes(PdfDict(x))
+ elif isinstance(x, list):
+ return bytes(PdfArray(x))
+ elif isinstance(x, str):
+ return pdf_repr(encode_text(x))
+ elif isinstance(x, bytes):
+ # XXX escape more chars? handle binary garbage
+ x = x.replace(b"\\", b"\\\\")
+ x = x.replace(b"(", b"\\(")
+ x = x.replace(b")", b"\\)")
+ return b"(" + x + b")"
+ else:
+ return bytes(x)
+
+
+class PdfParser:
+ """Based on
+ https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf
+ Supports PDF up to 1.4
+ """
+
+ def __init__(
+ self,
+ filename: str | None = None,
+ f: IO[bytes] | None = None,
+ buf: bytes | bytearray | None = None,
+ start_offset: int = 0,
+ mode: str = "rb",
+ ) -> None:
+ if buf and f:
+ msg = "specify buf or f or filename, but not both buf and f"
+ raise RuntimeError(msg)
+ self.filename = filename
+ self.buf: bytes | bytearray | memoryview | mmap.mmap | None = buf
+ self.f = f
+ self.start_offset = start_offset
+ self.should_close_buf = False
+ self.should_close_file = False
+ if filename is not None and f is None:
+ self.f = f = open(filename, mode)
+ self.should_close_file = True
+ if f is not None:
+ self.buf = self.get_buf_from_file(f)
+ self.should_close_buf = True
+ if not filename and hasattr(f, "name"):
+ self.filename = f.name
+ self.cached_objects: dict[IndirectReference, Any] = {}
+ self.root_ref: IndirectReference | None
+ self.info_ref: IndirectReference | None
+ self.pages_ref: IndirectReference | None
+ self.last_xref_section_offset: int | None
+ if self.buf:
+ try:
+ self.read_pdf_info()
+ except PdfFormatError:
+ self.close()
+ raise
+ else:
+ self.file_size_total = self.file_size_this = 0
+ self.root = PdfDict()
+ self.root_ref = None
+ self.info = PdfDict()
+ self.info_ref = None
+ self.page_tree_root = PdfDict()
+ self.pages: list[IndirectReference] = []
+ self.orig_pages: list[IndirectReference] = []
+ self.pages_ref = None
+ self.last_xref_section_offset = None
+ self.trailer_dict: dict[bytes, Any] = {}
+ self.xref_table = XrefTable()
+ self.xref_table.reading_finished = True
+ if f:
+ self.seek_end()
+
+ def __enter__(self) -> PdfParser:
+ return self
+
+ def __exit__(self, *args: object) -> None:
+ self.close()
+
+ def start_writing(self) -> None:
+ self.close_buf()
+ self.seek_end()
+
+ def close_buf(self) -> None:
+ if isinstance(self.buf, memoryview):
+ self.buf.release()
+ elif isinstance(self.buf, mmap.mmap):
+ self.buf.close()
+ self.buf = None
+
+ def close(self) -> None:
+ if self.should_close_buf:
+ self.close_buf()
+ if self.f is not None and self.should_close_file:
+ self.f.close()
+ self.f = None
+
+ def seek_end(self) -> None:
+ assert self.f is not None
+ self.f.seek(0, os.SEEK_END)
+
+ def write_header(self) -> None:
+ assert self.f is not None
+ self.f.write(b"%PDF-1.4\n")
+
+ def write_comment(self, s: str) -> None:
+ assert self.f is not None
+ self.f.write(f"% {s}\n".encode())
+
+ def write_catalog(self) -> IndirectReference:
+ assert self.f is not None
+ self.del_root()
+ self.root_ref = self.next_object_id(self.f.tell())
+ self.pages_ref = self.next_object_id(0)
+ self.rewrite_pages()
+ self.write_obj(self.root_ref, Type=PdfName(b"Catalog"), Pages=self.pages_ref)
+ self.write_obj(
+ self.pages_ref,
+ Type=PdfName(b"Pages"),
+ Count=len(self.pages),
+ Kids=self.pages,
+ )
+ return self.root_ref
+
+ def rewrite_pages(self) -> None:
+ pages_tree_nodes_to_delete = []
+ for i, page_ref in enumerate(self.orig_pages):
+ page_info = self.cached_objects[page_ref]
+ del self.xref_table[page_ref.object_id]
+ pages_tree_nodes_to_delete.append(page_info[PdfName(b"Parent")])
+ if page_ref not in self.pages:
+ # the page has been deleted
+ continue
+ # make dict keys into strings for passing to write_page
+ stringified_page_info = {}
+ for key, value in page_info.items():
+ # key should be a PdfName
+ stringified_page_info[key.name_as_str()] = value
+ stringified_page_info["Parent"] = self.pages_ref
+ new_page_ref = self.write_page(None, **stringified_page_info)
+ for j, cur_page_ref in enumerate(self.pages):
+ if cur_page_ref == page_ref:
+ # replace the page reference with the new one
+ self.pages[j] = new_page_ref
+ # delete redundant Pages tree nodes from xref table
+ for pages_tree_node_ref in pages_tree_nodes_to_delete:
+ while pages_tree_node_ref:
+ pages_tree_node = self.cached_objects[pages_tree_node_ref]
+ if pages_tree_node_ref.object_id in self.xref_table:
+ del self.xref_table[pages_tree_node_ref.object_id]
+ pages_tree_node_ref = pages_tree_node.get(b"Parent", None)
+ self.orig_pages = []
+
+ def write_xref_and_trailer(
+ self, new_root_ref: IndirectReference | None = None
+ ) -> None:
+ assert self.f is not None
+ if new_root_ref:
+ self.del_root()
+ self.root_ref = new_root_ref
+ if self.info:
+ self.info_ref = self.write_obj(None, self.info)
+ start_xref = self.xref_table.write(self.f)
+ num_entries = len(self.xref_table)
+ trailer_dict: dict[str | bytes, Any] = {
+ b"Root": self.root_ref,
+ b"Size": num_entries,
+ }
+ if self.last_xref_section_offset is not None:
+ trailer_dict[b"Prev"] = self.last_xref_section_offset
+ if self.info:
+ trailer_dict[b"Info"] = self.info_ref
+ self.last_xref_section_offset = start_xref
+ self.f.write(
+ b"trailer\n"
+ + bytes(PdfDict(trailer_dict))
+ + b"\nstartxref\n%d\n%%%%EOF" % start_xref
+ )
+
+ def write_page(
+ self, ref: int | IndirectReference | None, *objs: Any, **dict_obj: Any
+ ) -> IndirectReference:
+ obj_ref = self.pages[ref] if isinstance(ref, int) else ref
+ if "Type" not in dict_obj:
+ dict_obj["Type"] = PdfName(b"Page")
+ if "Parent" not in dict_obj:
+ dict_obj["Parent"] = self.pages_ref
+ return self.write_obj(obj_ref, *objs, **dict_obj)
+
+ def write_obj(
+ self, ref: IndirectReference | None, *objs: Any, **dict_obj: Any
+ ) -> IndirectReference:
+ assert self.f is not None
+ f = self.f
+ if ref is None:
+ ref = self.next_object_id(f.tell())
+ else:
+ self.xref_table[ref.object_id] = (f.tell(), ref.generation)
+ f.write(bytes(IndirectObjectDef(*ref)))
+ stream = dict_obj.pop("stream", None)
+ if stream is not None:
+ dict_obj["Length"] = len(stream)
+ if dict_obj:
+ f.write(pdf_repr(dict_obj))
+ for obj in objs:
+ f.write(pdf_repr(obj))
+ if stream is not None:
+ f.write(b"stream\n")
+ f.write(stream)
+ f.write(b"\nendstream\n")
+ f.write(b"endobj\n")
+ return ref
+
+ def del_root(self) -> None:
+ if self.root_ref is None:
+ return
+ del self.xref_table[self.root_ref.object_id]
+ del self.xref_table[self.root[b"Pages"].object_id]
+
+ @staticmethod
+ def get_buf_from_file(f: IO[bytes]) -> bytes | mmap.mmap:
+ if hasattr(f, "getbuffer"):
+ return f.getbuffer()
+ elif hasattr(f, "getvalue"):
+ return f.getvalue()
+ else:
+ try:
+ return mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
+ except ValueError: # cannot mmap an empty file
+ return b""
+
+ def read_pdf_info(self) -> None:
+ assert self.buf is not None
+ self.file_size_total = len(self.buf)
+ self.file_size_this = self.file_size_total - self.start_offset
+ self.read_trailer()
+ check_format_condition(
+ self.trailer_dict.get(b"Root") is not None, "Root is missing"
+ )
+ self.root_ref = self.trailer_dict[b"Root"]
+ assert self.root_ref is not None
+ self.info_ref = self.trailer_dict.get(b"Info", None)
+ self.root = PdfDict(self.read_indirect(self.root_ref))
+ if self.info_ref is None:
+ self.info = PdfDict()
+ else:
+ self.info = PdfDict(self.read_indirect(self.info_ref))
+ check_format_condition(b"Type" in self.root, "/Type missing in Root")
+ check_format_condition(
+ self.root[b"Type"] == b"Catalog", "/Type in Root is not /Catalog"
+ )
+ check_format_condition(
+ self.root.get(b"Pages") is not None, "/Pages missing in Root"
+ )
+ check_format_condition(
+ isinstance(self.root[b"Pages"], IndirectReference),
+ "/Pages in Root is not an indirect reference",
+ )
+ self.pages_ref = self.root[b"Pages"]
+ assert self.pages_ref is not None
+ self.page_tree_root = self.read_indirect(self.pages_ref)
+ self.pages = self.linearize_page_tree(self.page_tree_root)
+ # save the original list of page references
+ # in case the user modifies, adds or deletes some pages
+ # and we need to rewrite the pages and their list
+ self.orig_pages = self.pages[:]
+
+ def next_object_id(self, offset: int | None = None) -> IndirectReference:
+ try:
+ # TODO: support reuse of deleted objects
+ reference = IndirectReference(max(self.xref_table.keys()) + 1, 0)
+ except ValueError:
+ reference = IndirectReference(1, 0)
+ if offset is not None:
+ self.xref_table[reference.object_id] = (offset, 0)
+ return reference
+
+ delimiter = rb"[][()<>{}/%]"
+ delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]"
+ whitespace = rb"[\000\011\012\014\015\040]"
+ whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]"
+ whitespace_optional = whitespace + b"*"
+ whitespace_mandatory = whitespace + b"+"
+ # No "\012" aka "\n" or "\015" aka "\r":
+ whitespace_optional_no_nl = rb"[\000\011\014\040]*"
+ newline_only = rb"[\r\n]+"
+ newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl
+ re_trailer_end = re.compile(
+ whitespace_mandatory
+ + rb"trailer"
+ + whitespace_optional
+ + rb"<<(.*>>)"
+ + newline
+ + rb"startxref"
+ + newline
+ + rb"([0-9]+)"
+ + newline
+ + rb"%%EOF"
+ + whitespace_optional
+ + rb"$",
+ re.DOTALL,
+ )
+ re_trailer_prev = re.compile(
+ whitespace_optional
+ + rb"trailer"
+ + whitespace_optional
+ + rb"<<(.*?>>)"
+ + newline
+ + rb"startxref"
+ + newline
+ + rb"([0-9]+)"
+ + newline
+ + rb"%%EOF"
+ + whitespace_optional,
+ re.DOTALL,
+ )
+
+ def read_trailer(self) -> None:
+ assert self.buf is not None
+ search_start_offset = len(self.buf) - 16384
+ if search_start_offset < self.start_offset:
+ search_start_offset = self.start_offset
+ m = self.re_trailer_end.search(self.buf, search_start_offset)
+ check_format_condition(m is not None, "trailer end not found")
+ # make sure we found the LAST trailer
+ last_match = m
+ while m:
+ last_match = m
+ m = self.re_trailer_end.search(self.buf, m.start() + 16)
+ if not m:
+ m = last_match
+ assert m is not None
+ trailer_data = m.group(1)
+ self.last_xref_section_offset = int(m.group(2))
+ self.trailer_dict = self.interpret_trailer(trailer_data)
+ self.xref_table = XrefTable()
+ self.read_xref_table(xref_section_offset=self.last_xref_section_offset)
+ if b"Prev" in self.trailer_dict:
+ self.read_prev_trailer(self.trailer_dict[b"Prev"])
+
+ def read_prev_trailer(
+ self, xref_section_offset: int, processed_offsets: list[int] | None = None
+ ) -> None:
+ assert self.buf is not None
+ trailer_offset = self.read_xref_table(xref_section_offset=xref_section_offset)
+ m = self.re_trailer_prev.search(
+ self.buf[trailer_offset : trailer_offset + 16384]
+ )
+ check_format_condition(m is not None, "previous trailer not found")
+ assert m is not None
+ trailer_data = m.group(1)
+ check_format_condition(
+ int(m.group(2)) == xref_section_offset,
+ "xref section offset in previous trailer doesn't match what was expected",
+ )
+ trailer_dict = self.interpret_trailer(trailer_data)
+ if b"Prev" in trailer_dict:
+ if processed_offsets is None:
+ processed_offsets = []
+ processed_offsets.append(xref_section_offset)
+ check_format_condition(
+ trailer_dict[b"Prev"] not in processed_offsets, "trailer loop found"
+ )
+ self.read_prev_trailer(trailer_dict[b"Prev"], processed_offsets)
+
+ re_whitespace_optional = re.compile(whitespace_optional)
+ re_name = re.compile(
+ whitespace_optional
+ + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?="
+ + delimiter_or_ws
+ + rb")"
+ )
+ re_dict_start = re.compile(whitespace_optional + rb"<<")
+ re_dict_end = re.compile(whitespace_optional + rb">>" + whitespace_optional)
+
+ @classmethod
+ def interpret_trailer(cls, trailer_data: bytes) -> dict[bytes, Any]:
+ trailer = {}
+ offset = 0
+ while True:
+ m = cls.re_name.match(trailer_data, offset)
+ if not m:
+ m = cls.re_dict_end.match(trailer_data, offset)
+ check_format_condition(
+ m is not None and m.end() == len(trailer_data),
+ "name not found in trailer, remaining data: "
+ + repr(trailer_data[offset:]),
+ )
+ break
+ key = cls.interpret_name(m.group(1))
+ assert isinstance(key, bytes)
+ value, value_offset = cls.get_value(trailer_data, m.end())
+ trailer[key] = value
+ if value_offset is None:
+ break
+ offset = value_offset
+ check_format_condition(
+ b"Size" in trailer and isinstance(trailer[b"Size"], int),
+ "/Size not in trailer or not an integer",
+ )
+ check_format_condition(
+ b"Root" in trailer and isinstance(trailer[b"Root"], IndirectReference),
+ "/Root not in trailer or not an indirect reference",
+ )
+ return trailer
+
+ re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?")
+
+ @classmethod
+ def interpret_name(cls, raw: bytes, as_text: bool = False) -> str | bytes:
+ name = b""
+ for m in cls.re_hashes_in_name.finditer(raw):
+ if m.group(3):
+ name += m.group(1) + bytearray.fromhex(m.group(3).decode("us-ascii"))
+ else:
+ name += m.group(1)
+ if as_text:
+ return name.decode("utf-8")
+ else:
+ return bytes(name)
+
+ re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")")
+ re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")")
+ re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")")
+ re_int = re.compile(
+ whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")"
+ )
+ re_real = re.compile(
+ whitespace_optional
+ + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?="
+ + delimiter_or_ws
+ + rb")"
+ )
+ re_array_start = re.compile(whitespace_optional + rb"\[")
+ re_array_end = re.compile(whitespace_optional + rb"]")
+ re_string_hex = re.compile(
+ whitespace_optional + rb"<(" + whitespace_or_hex + rb"*)>"
+ )
+ re_string_lit = re.compile(whitespace_optional + rb"\(")
+ re_indirect_reference = re.compile(
+ whitespace_optional
+ + rb"([-+]?[0-9]+)"
+ + whitespace_mandatory
+ + rb"([-+]?[0-9]+)"
+ + whitespace_mandatory
+ + rb"R(?="
+ + delimiter_or_ws
+ + rb")"
+ )
+ re_indirect_def_start = re.compile(
+ whitespace_optional
+ + rb"([-+]?[0-9]+)"
+ + whitespace_mandatory
+ + rb"([-+]?[0-9]+)"
+ + whitespace_mandatory
+ + rb"obj(?="
+ + delimiter_or_ws
+ + rb")"
+ )
+ re_indirect_def_end = re.compile(
+ whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")"
+ )
+ re_comment = re.compile(
+ rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*"
+ )
+ re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n")
+ re_stream_end = re.compile(
+ whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")"
+ )
+
+ @classmethod
+ def get_value(
+ cls,
+ data: bytes | bytearray | memoryview | mmap.mmap,
+ offset: int,
+ expect_indirect: IndirectReference | None = None,
+ max_nesting: int = -1,
+ ) -> tuple[Any, int | None]:
+ if max_nesting == 0:
+ return None, None
+ m = cls.re_comment.match(data, offset)
+ if m:
+ offset = m.end()
+ m = cls.re_indirect_def_start.match(data, offset)
+ if m:
+ check_format_condition(
+ int(m.group(1)) > 0,
+ "indirect object definition: object ID must be greater than 0",
+ )
+ check_format_condition(
+ int(m.group(2)) >= 0,
+ "indirect object definition: generation must be non-negative",
+ )
+ check_format_condition(
+ expect_indirect is None
+ or expect_indirect
+ == IndirectReference(int(m.group(1)), int(m.group(2))),
+ "indirect object definition different than expected",
+ )
+ object, object_offset = cls.get_value(
+ data, m.end(), max_nesting=max_nesting - 1
+ )
+ if object_offset is None:
+ return object, None
+ m = cls.re_indirect_def_end.match(data, object_offset)
+ check_format_condition(
+ m is not None, "indirect object definition end not found"
+ )
+ assert m is not None
+ return object, m.end()
+ check_format_condition(
+ not expect_indirect, "indirect object definition not found"
+ )
+ m = cls.re_indirect_reference.match(data, offset)
+ if m:
+ check_format_condition(
+ int(m.group(1)) > 0,
+ "indirect object reference: object ID must be greater than 0",
+ )
+ check_format_condition(
+ int(m.group(2)) >= 0,
+ "indirect object reference: generation must be non-negative",
+ )
+ return IndirectReference(int(m.group(1)), int(m.group(2))), m.end()
+ m = cls.re_dict_start.match(data, offset)
+ if m:
+ offset = m.end()
+ result: dict[Any, Any] = {}
+ m = cls.re_dict_end.match(data, offset)
+ current_offset: int | None = offset
+ while not m:
+ assert current_offset is not None
+ key, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
+ if current_offset is None:
+ return result, None
+ value, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
+ result[key] = value
+ if current_offset is None:
+ return result, None
+ m = cls.re_dict_end.match(data, current_offset)
+ current_offset = m.end()
+ m = cls.re_stream_start.match(data, current_offset)
+ if m:
+ stream_len = result.get(b"Length")
+ if stream_len is None or not isinstance(stream_len, int):
+ msg = f"bad or missing Length in stream dict ({stream_len})"
+ raise PdfFormatError(msg)
+ stream_data = bytes(data[m.end() : m.end() + stream_len])
+ m = cls.re_stream_end.match(data, m.end() + stream_len)
+ check_format_condition(m is not None, "stream end not found")
+ assert m is not None
+ current_offset = m.end()
+ return PdfStream(PdfDict(result), stream_data), current_offset
+ return PdfDict(result), current_offset
+ m = cls.re_array_start.match(data, offset)
+ if m:
+ offset = m.end()
+ results = []
+ m = cls.re_array_end.match(data, offset)
+ current_offset = offset
+ while not m:
+ assert current_offset is not None
+ value, current_offset = cls.get_value(
+ data, current_offset, max_nesting=max_nesting - 1
+ )
+ results.append(value)
+ if current_offset is None:
+ return results, None
+ m = cls.re_array_end.match(data, current_offset)
+ return results, m.end()
+ m = cls.re_null.match(data, offset)
+ if m:
+ return None, m.end()
+ m = cls.re_true.match(data, offset)
+ if m:
+ return True, m.end()
+ m = cls.re_false.match(data, offset)
+ if m:
+ return False, m.end()
+ m = cls.re_name.match(data, offset)
+ if m:
+ return PdfName(cls.interpret_name(m.group(1))), m.end()
+ m = cls.re_int.match(data, offset)
+ if m:
+ return int(m.group(1)), m.end()
+ m = cls.re_real.match(data, offset)
+ if m:
+ # XXX Decimal instead of float???
+ return float(m.group(1)), m.end()
+ m = cls.re_string_hex.match(data, offset)
+ if m:
+ # filter out whitespace
+ hex_string = bytearray(
+ b for b in m.group(1) if b in b"0123456789abcdefABCDEF"
+ )
+ if len(hex_string) % 2 == 1:
+ # append a 0 if the length is not even - yes, at the end
+ hex_string.append(ord(b"0"))
+ return bytearray.fromhex(hex_string.decode("us-ascii")), m.end()
+ m = cls.re_string_lit.match(data, offset)
+ if m:
+ return cls.get_literal_string(data, m.end())
+ # return None, offset # fallback (only for debugging)
+ msg = f"unrecognized object: {repr(data[offset : offset + 32])}"
+ raise PdfFormatError(msg)
+
+ re_lit_str_token = re.compile(
+ rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))"
+ )
+ escaped_chars = {
+ b"n": b"\n",
+ b"r": b"\r",
+ b"t": b"\t",
+ b"b": b"\b",
+ b"f": b"\f",
+ b"(": b"(",
+ b")": b")",
+ b"\\": b"\\",
+ ord(b"n"): b"\n",
+ ord(b"r"): b"\r",
+ ord(b"t"): b"\t",
+ ord(b"b"): b"\b",
+ ord(b"f"): b"\f",
+ ord(b"("): b"(",
+ ord(b")"): b")",
+ ord(b"\\"): b"\\",
+ }
+
+ @classmethod
+ def get_literal_string(
+ cls, data: bytes | bytearray | memoryview | mmap.mmap, offset: int
+ ) -> tuple[bytes, int]:
+ nesting_depth = 0
+ result = bytearray()
+ for m in cls.re_lit_str_token.finditer(data, offset):
+ result.extend(data[offset : m.start()])
+ if m.group(1):
+ result.extend(cls.escaped_chars[m.group(1)[1]])
+ elif m.group(2):
+ result.append(int(m.group(2)[1:], 8))
+ elif m.group(3):
+ pass
+ elif m.group(5):
+ result.extend(b"\n")
+ elif m.group(6):
+ result.extend(b"(")
+ nesting_depth += 1
+ elif m.group(7):
+ if nesting_depth == 0:
+ return bytes(result), m.end()
+ result.extend(b")")
+ nesting_depth -= 1
+ offset = m.end()
+ msg = "unfinished literal string"
+ raise PdfFormatError(msg)
+
+ re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline)
+ re_xref_subsection_start = re.compile(
+ whitespace_optional
+ + rb"([0-9]+)"
+ + whitespace_mandatory
+ + rb"([0-9]+)"
+ + whitespace_optional
+ + newline_only
+ )
+ re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)")
+
+ def read_xref_table(self, xref_section_offset: int) -> int:
+ assert self.buf is not None
+ subsection_found = False
+ m = self.re_xref_section_start.match(
+ self.buf, xref_section_offset + self.start_offset
+ )
+ check_format_condition(m is not None, "xref section start not found")
+ assert m is not None
+ offset = m.end()
+ while True:
+ m = self.re_xref_subsection_start.match(self.buf, offset)
+ if not m:
+ check_format_condition(
+ subsection_found, "xref subsection start not found"
+ )
+ break
+ subsection_found = True
+ offset = m.end()
+ first_object = int(m.group(1))
+ num_objects = int(m.group(2))
+ for i in range(first_object, first_object + num_objects):
+ m = self.re_xref_entry.match(self.buf, offset)
+ check_format_condition(m is not None, "xref entry not found")
+ assert m is not None
+ offset = m.end()
+ is_free = m.group(3) == b"f"
+ if not is_free:
+ generation = int(m.group(2))
+ new_entry = (int(m.group(1)), generation)
+ if i not in self.xref_table:
+ self.xref_table[i] = new_entry
+ return offset
+
+ def read_indirect(self, ref: IndirectReference, max_nesting: int = -1) -> Any:
+ offset, generation = self.xref_table[ref[0]]
+ check_format_condition(
+ generation == ref[1],
+ f"expected to find generation {ref[1]} for object ID {ref[0]} in xref "
+ f"table, instead found generation {generation} at offset {offset}",
+ )
+ assert self.buf is not None
+ value = self.get_value(
+ self.buf,
+ offset + self.start_offset,
+ expect_indirect=IndirectReference(*ref),
+ max_nesting=max_nesting,
+ )[0]
+ self.cached_objects[ref] = value
+ return value
+
+ def linearize_page_tree(
+ self, node: PdfDict | None = None
+ ) -> list[IndirectReference]:
+ page_node = node if node is not None else self.page_tree_root
+ check_format_condition(
+ page_node[b"Type"] == b"Pages", "/Type of page tree node is not /Pages"
+ )
+ pages = []
+ for kid in page_node[b"Kids"]:
+ kid_object = self.read_indirect(kid)
+ if kid_object[b"Type"] == b"Page":
+ pages.append(kid)
+ else:
+ pages.extend(self.linearize_page_tree(node=kid_object))
+ return pages
diff --git a/venv/lib/python3.11/site-packages/PIL/PixarImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PixarImagePlugin.py
new file mode 100644
index 000000000..d2b6d0a97
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PixarImagePlugin.py
@@ -0,0 +1,72 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# PIXAR raster support for PIL
+#
+# history:
+# 97-01-29 fl Created
+#
+# notes:
+# This is incomplete; it is based on a few samples created with
+# Photoshop 2.5 and 3.0, and a summary description provided by
+# Greg Coats . Hopefully, "L" and
+# "RGBA" support will be added in future versions.
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1997.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from . import Image, ImageFile
+from ._binary import i16le as i16
+
+#
+# helpers
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(b"\200\350\000\000")
+
+
+##
+# Image plugin for PIXAR raster images.
+
+
+class PixarImageFile(ImageFile.ImageFile):
+ format = "PIXAR"
+ format_description = "PIXAR raster image"
+
+ def _open(self) -> None:
+ # assuming a 4-byte magic label
+ assert self.fp is not None
+
+ s = self.fp.read(4)
+ if not _accept(s):
+ msg = "not a PIXAR file"
+ raise SyntaxError(msg)
+
+ # read rest of header
+ s = s + self.fp.read(508)
+
+ self._size = i16(s, 418), i16(s, 416)
+
+ # get channel/depth descriptions
+ mode = i16(s, 424), i16(s, 426)
+
+ if mode == (14, 2):
+ self._mode = "RGB"
+ # FIXME: to be continued...
+
+ # create tile descriptor (assuming "dumped")
+ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 1024, self.mode)]
+
+
+#
+# --------------------------------------------------------------------
+
+Image.register_open(PixarImageFile.format, PixarImageFile, _accept)
+
+Image.register_extension(PixarImageFile.format, ".pxr")
diff --git a/venv/lib/python3.11/site-packages/PIL/PngImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PngImagePlugin.py
new file mode 100644
index 000000000..058fb831c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PngImagePlugin.py
@@ -0,0 +1,1578 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# PNG support code
+#
+# See "PNG (Portable Network Graphics) Specification, version 1.0;
+# W3C Recommendation", 1996-10-01, Thomas Boutell (ed.).
+#
+# history:
+# 1996-05-06 fl Created (couldn't resist it)
+# 1996-12-14 fl Upgraded, added read and verify support (0.2)
+# 1996-12-15 fl Separate PNG stream parser
+# 1996-12-29 fl Added write support, added getchunks
+# 1996-12-30 fl Eliminated circular references in decoder (0.3)
+# 1998-07-12 fl Read/write 16-bit images as mode I (0.4)
+# 2001-02-08 fl Added transparency support (from Zircon) (0.5)
+# 2001-04-16 fl Don't close data source in "open" method (0.6)
+# 2004-02-24 fl Don't even pretend to support interlaced files (0.7)
+# 2004-08-31 fl Do basic sanity check on chunk identifiers (0.8)
+# 2004-09-20 fl Added PngInfo chunk container
+# 2004-12-18 fl Added DPI read support (based on code by Niki Spahiev)
+# 2008-08-13 fl Added tRNS support for RGB images
+# 2009-03-06 fl Support for preserving ICC profiles (by Florian Hoech)
+# 2009-03-08 fl Added zTXT support (from Lowell Alleman)
+# 2009-03-29 fl Read interlaced PNG files (from Conrado Porto Lopes Gouvua)
+#
+# Copyright (c) 1997-2009 by Secret Labs AB
+# Copyright (c) 1996 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import itertools
+import logging
+import re
+import struct
+import warnings
+import zlib
+from enum import IntEnum
+from fractions import Fraction
+from typing import IO, NamedTuple, cast
+
+from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
+from ._binary import i16be as i16
+from ._binary import i32be as i32
+from ._binary import o8
+from ._binary import o16be as o16
+from ._binary import o32be as o32
+from ._deprecate import deprecate
+from ._util import DeferredError
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Any, NoReturn
+
+ from . import _imaging
+
+logger = logging.getLogger(__name__)
+
+is_cid = re.compile(rb"\w\w\w\w").match
+
+
+_MAGIC = b"\211PNG\r\n\032\n"
+
+
+_MODES = {
+ # supported bits/color combinations, and corresponding modes/rawmodes
+ # Grayscale
+ (1, 0): ("1", "1"),
+ (2, 0): ("L", "L;2"),
+ (4, 0): ("L", "L;4"),
+ (8, 0): ("L", "L"),
+ (16, 0): ("I;16", "I;16B"),
+ # Truecolour
+ (8, 2): ("RGB", "RGB"),
+ (16, 2): ("RGB", "RGB;16B"),
+ # Indexed-colour
+ (1, 3): ("P", "P;1"),
+ (2, 3): ("P", "P;2"),
+ (4, 3): ("P", "P;4"),
+ (8, 3): ("P", "P"),
+ # Grayscale with alpha
+ (8, 4): ("LA", "LA"),
+ (16, 4): ("RGBA", "LA;16B"), # LA;16B->LA not yet available
+ # Truecolour with alpha
+ (8, 6): ("RGBA", "RGBA"),
+ (16, 6): ("RGBA", "RGBA;16B"),
+}
+
+
+_simple_palette = re.compile(b"^\xff*\x00\xff*$")
+
+MAX_TEXT_CHUNK = ImageFile.SAFEBLOCK
+"""
+Maximum decompressed size for a iTXt or zTXt chunk.
+Eliminates decompression bombs where compressed chunks can expand 1000x.
+See :ref:`Text in PNG File Format`.
+"""
+MAX_TEXT_MEMORY = 64 * MAX_TEXT_CHUNK
+"""
+Set the maximum total text chunk size.
+See :ref:`Text in PNG File Format`.
+"""
+
+
+# APNG frame disposal modes
+class Disposal(IntEnum):
+ OP_NONE = 0
+ """
+ No disposal is done on this frame before rendering the next frame.
+ See :ref:`Saving APNG sequences`.
+ """
+ OP_BACKGROUND = 1
+ """
+ This frame’s modified region is cleared to fully transparent black before rendering
+ the next frame.
+ See :ref:`Saving APNG sequences`.
+ """
+ OP_PREVIOUS = 2
+ """
+ This frame’s modified region is reverted to the previous frame’s contents before
+ rendering the next frame.
+ See :ref:`Saving APNG sequences`.
+ """
+
+
+# APNG frame blend modes
+class Blend(IntEnum):
+ OP_SOURCE = 0
+ """
+ All color components of this frame, including alpha, overwrite the previous output
+ image contents.
+ See :ref:`Saving APNG sequences`.
+ """
+ OP_OVER = 1
+ """
+ This frame should be alpha composited with the previous output image contents.
+ See :ref:`Saving APNG sequences`.
+ """
+
+
+def _safe_zlib_decompress(s: bytes) -> bytes:
+ dobj = zlib.decompressobj()
+ plaintext = dobj.decompress(s, MAX_TEXT_CHUNK)
+ if dobj.unconsumed_tail:
+ msg = "Decompressed data too large for PngImagePlugin.MAX_TEXT_CHUNK"
+ raise ValueError(msg)
+ return plaintext
+
+
+def _crc32(data: bytes, seed: int = 0) -> int:
+ return zlib.crc32(data, seed) & 0xFFFFFFFF
+
+
+# --------------------------------------------------------------------
+# Support classes. Suitable for PNG and related formats like MNG etc.
+
+
+class ChunkStream:
+ def __init__(self, fp: IO[bytes]) -> None:
+ self.fp: IO[bytes] | None = fp
+ self.queue: list[tuple[bytes, int, int]] | None = []
+
+ def read(self) -> tuple[bytes, int, int]:
+ """Fetch a new chunk. Returns header information."""
+ cid = None
+
+ assert self.fp is not None
+ if self.queue:
+ cid, pos, length = self.queue.pop()
+ self.fp.seek(pos)
+ else:
+ s = self.fp.read(8)
+ cid = s[4:]
+ pos = self.fp.tell()
+ length = i32(s)
+
+ if not is_cid(cid):
+ if not ImageFile.LOAD_TRUNCATED_IMAGES:
+ msg = f"broken PNG file (chunk {repr(cid)})"
+ raise SyntaxError(msg)
+
+ return cid, pos, length
+
+ def __enter__(self) -> ChunkStream:
+ return self
+
+ def __exit__(self, *args: object) -> None:
+ self.close()
+
+ def close(self) -> None:
+ self.queue = self.fp = None
+
+ def push(self, cid: bytes, pos: int, length: int) -> None:
+ assert self.queue is not None
+ self.queue.append((cid, pos, length))
+
+ def call(self, cid: bytes, pos: int, length: int) -> bytes:
+ """Call the appropriate chunk handler"""
+
+ logger.debug("STREAM %r %s %s", cid, pos, length)
+ return getattr(self, f"chunk_{cid.decode('ascii')}")(pos, length)
+
+ def crc(self, cid: bytes, data: bytes) -> None:
+ """Read and verify checksum"""
+
+ # Skip CRC checks for ancillary chunks if allowed to load truncated
+ # images
+ # 5th byte of first char is 1 [specs, section 5.4]
+ if ImageFile.LOAD_TRUNCATED_IMAGES and (cid[0] >> 5 & 1):
+ self.crc_skip(cid, data)
+ return
+
+ assert self.fp is not None
+ try:
+ crc1 = _crc32(data, _crc32(cid))
+ crc2 = i32(self.fp.read(4))
+ if crc1 != crc2:
+ msg = f"broken PNG file (bad header checksum in {repr(cid)})"
+ raise SyntaxError(msg)
+ except struct.error as e:
+ msg = f"broken PNG file (incomplete checksum in {repr(cid)})"
+ raise SyntaxError(msg) from e
+
+ def crc_skip(self, cid: bytes, data: bytes) -> None:
+ """Read checksum"""
+
+ assert self.fp is not None
+ self.fp.read(4)
+
+ def verify(self, endchunk: bytes = b"IEND") -> list[bytes]:
+ # Simple approach; just calculate checksum for all remaining
+ # blocks. Must be called directly after open.
+
+ cids = []
+
+ assert self.fp is not None
+ while True:
+ try:
+ cid, pos, length = self.read()
+ except struct.error as e:
+ msg = "truncated PNG file"
+ raise OSError(msg) from e
+
+ if cid == endchunk:
+ break
+ self.crc(cid, ImageFile._safe_read(self.fp, length))
+ cids.append(cid)
+
+ return cids
+
+
+class iTXt(str):
+ """
+ Subclass of string to allow iTXt chunks to look like strings while
+ keeping their extra information
+
+ """
+
+ lang: str | bytes | None
+ tkey: str | bytes | None
+
+ @staticmethod
+ def __new__(
+ cls, text: str, lang: str | None = None, tkey: str | None = None
+ ) -> iTXt:
+ """
+ :param cls: the class to use when creating the instance
+ :param text: value for this key
+ :param lang: language code
+ :param tkey: UTF-8 version of the key name
+ """
+
+ self = str.__new__(cls, text)
+ self.lang = lang
+ self.tkey = tkey
+ return self
+
+
+class PngInfo:
+ """
+ PNG chunk container (for use with save(pnginfo=))
+
+ """
+
+ def __init__(self) -> None:
+ self.chunks: list[tuple[bytes, bytes, bool]] = []
+
+ def add(self, cid: bytes, data: bytes, after_idat: bool = False) -> None:
+ """Appends an arbitrary chunk. Use with caution.
+
+ :param cid: a byte string, 4 bytes long.
+ :param data: a byte string of the encoded data
+ :param after_idat: for use with private chunks. Whether the chunk
+ should be written after IDAT
+
+ """
+
+ self.chunks.append((cid, data, after_idat))
+
+ def add_itxt(
+ self,
+ key: str | bytes,
+ value: str | bytes,
+ lang: str | bytes = "",
+ tkey: str | bytes = "",
+ zip: bool = False,
+ ) -> None:
+ """Appends an iTXt chunk.
+
+ :param key: latin-1 encodable text key name
+ :param value: value for this key
+ :param lang: language code
+ :param tkey: UTF-8 version of the key name
+ :param zip: compression flag
+
+ """
+
+ if not isinstance(key, bytes):
+ key = key.encode("latin-1", "strict")
+ if not isinstance(value, bytes):
+ value = value.encode("utf-8", "strict")
+ if not isinstance(lang, bytes):
+ lang = lang.encode("utf-8", "strict")
+ if not isinstance(tkey, bytes):
+ tkey = tkey.encode("utf-8", "strict")
+
+ if zip:
+ self.add(
+ b"iTXt",
+ key + b"\0\x01\0" + lang + b"\0" + tkey + b"\0" + zlib.compress(value),
+ )
+ else:
+ self.add(b"iTXt", key + b"\0\0\0" + lang + b"\0" + tkey + b"\0" + value)
+
+ def add_text(
+ self, key: str | bytes, value: str | bytes | iTXt, zip: bool = False
+ ) -> None:
+ """Appends a text chunk.
+
+ :param key: latin-1 encodable text key name
+ :param value: value for this key, text or an
+ :py:class:`PIL.PngImagePlugin.iTXt` instance
+ :param zip: compression flag
+
+ """
+ if isinstance(value, iTXt):
+ return self.add_itxt(
+ key,
+ value,
+ value.lang if value.lang is not None else b"",
+ value.tkey if value.tkey is not None else b"",
+ zip=zip,
+ )
+
+ # The tEXt chunk stores latin-1 text
+ if not isinstance(value, bytes):
+ try:
+ value = value.encode("latin-1", "strict")
+ except UnicodeError:
+ return self.add_itxt(key, value, zip=zip)
+
+ if not isinstance(key, bytes):
+ key = key.encode("latin-1", "strict")
+
+ if zip:
+ self.add(b"zTXt", key + b"\0\0" + zlib.compress(value))
+ else:
+ self.add(b"tEXt", key + b"\0" + value)
+
+
+# --------------------------------------------------------------------
+# PNG image stream (IHDR/IEND)
+
+
+class _RewindState(NamedTuple):
+ info: dict[str | tuple[int, int], Any]
+ tile: list[ImageFile._Tile]
+ seq_num: int | None
+
+
+class PngStream(ChunkStream):
+ def __init__(self, fp: IO[bytes]) -> None:
+ super().__init__(fp)
+
+ # local copies of Image attributes
+ self.im_info: dict[str | tuple[int, int], Any] = {}
+ self.im_text: dict[str, str | iTXt] = {}
+ self.im_size = (0, 0)
+ self.im_mode = ""
+ self.im_tile: list[ImageFile._Tile] = []
+ self.im_palette: tuple[str, bytes] | None = None
+ self.im_custom_mimetype: str | None = None
+ self.im_n_frames: int | None = None
+ self._seq_num: int | None = None
+ self.rewind_state = _RewindState({}, [], None)
+
+ self.text_memory = 0
+
+ def __enter__(self) -> PngStream:
+ return self
+
+ def check_text_memory(self, chunklen: int) -> None:
+ self.text_memory += chunklen
+ if self.text_memory > MAX_TEXT_MEMORY:
+ msg = (
+ "Too much memory used in text chunks: "
+ f"{self.text_memory}>MAX_TEXT_MEMORY"
+ )
+ raise ValueError(msg)
+
+ def save_rewind(self) -> None:
+ self.rewind_state = _RewindState(
+ self.im_info.copy(),
+ self.im_tile,
+ self._seq_num,
+ )
+
+ def rewind(self) -> None:
+ self.im_info = self.rewind_state.info.copy()
+ self.im_tile = self.rewind_state.tile
+ self._seq_num = self.rewind_state.seq_num
+
+ def chunk_iCCP(self, pos: int, length: int) -> bytes:
+ # ICC profile
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ # according to PNG spec, the iCCP chunk contains:
+ # Profile name 1-79 bytes (character string)
+ # Null separator 1 byte (null character)
+ # Compression method 1 byte (0)
+ # Compressed profile n bytes (zlib with deflate compression)
+ i = s.find(b"\0")
+ logger.debug("iCCP profile name %r", s[:i])
+ comp_method = s[i + 1]
+ logger.debug("Compression method %s", comp_method)
+ if comp_method != 0:
+ msg = f"Unknown compression method {comp_method} in iCCP chunk"
+ raise SyntaxError(msg)
+ try:
+ icc_profile = _safe_zlib_decompress(s[i + 2 :])
+ except ValueError:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ icc_profile = None
+ else:
+ raise
+ except zlib.error:
+ icc_profile = None # FIXME
+ self.im_info["icc_profile"] = icc_profile
+ return s
+
+ def chunk_IHDR(self, pos: int, length: int) -> bytes:
+ # image header
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ if length < 13:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ return s
+ msg = "Truncated IHDR chunk"
+ raise ValueError(msg)
+ self.im_size = i32(s, 0), i32(s, 4)
+ try:
+ self.im_mode, self.im_rawmode = _MODES[(s[8], s[9])]
+ except KeyError:
+ pass
+ if s[12]:
+ self.im_info["interlace"] = 1
+ if s[11]:
+ msg = "unknown filter category"
+ raise SyntaxError(msg)
+ return s
+
+ def chunk_IDAT(self, pos: int, length: int) -> NoReturn:
+ # image data
+ if "bbox" in self.im_info:
+ tile = [ImageFile._Tile("zip", self.im_info["bbox"], pos, self.im_rawmode)]
+ else:
+ if self.im_n_frames is not None:
+ self.im_info["default_image"] = True
+ tile = [ImageFile._Tile("zip", (0, 0) + self.im_size, pos, self.im_rawmode)]
+ self.im_tile = tile
+ self.im_idat = length
+ msg = "image data found"
+ raise EOFError(msg)
+
+ def chunk_IEND(self, pos: int, length: int) -> NoReturn:
+ msg = "end of PNG image"
+ raise EOFError(msg)
+
+ def chunk_PLTE(self, pos: int, length: int) -> bytes:
+ # palette
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ if self.im_mode == "P":
+ self.im_palette = "RGB", s
+ return s
+
+ def chunk_tRNS(self, pos: int, length: int) -> bytes:
+ # transparency
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ if self.im_mode == "P":
+ if _simple_palette.match(s):
+ # tRNS contains only one full-transparent entry,
+ # other entries are full opaque
+ i = s.find(b"\0")
+ if i >= 0:
+ self.im_info["transparency"] = i
+ else:
+ # otherwise, we have a byte string with one alpha value
+ # for each palette entry
+ self.im_info["transparency"] = s
+ elif self.im_mode == "1":
+ self.im_info["transparency"] = 255 if i16(s) else 0
+ elif self.im_mode in ("L", "I;16"):
+ self.im_info["transparency"] = i16(s)
+ elif self.im_mode == "RGB":
+ self.im_info["transparency"] = i16(s), i16(s, 2), i16(s, 4)
+ return s
+
+ def chunk_gAMA(self, pos: int, length: int) -> bytes:
+ # gamma setting
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ self.im_info["gamma"] = i32(s) / 100000.0
+ return s
+
+ def chunk_cHRM(self, pos: int, length: int) -> bytes:
+ # chromaticity, 8 unsigned ints, actual value is scaled by 100,000
+ # WP x,y, Red x,y, Green x,y Blue x,y
+
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ raw_vals = struct.unpack(f">{len(s) // 4}I", s)
+ self.im_info["chromaticity"] = tuple(elt / 100000.0 for elt in raw_vals)
+ return s
+
+ def chunk_sRGB(self, pos: int, length: int) -> bytes:
+ # srgb rendering intent, 1 byte
+ # 0 perceptual
+ # 1 relative colorimetric
+ # 2 saturation
+ # 3 absolute colorimetric
+
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ if length < 1:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ return s
+ msg = "Truncated sRGB chunk"
+ raise ValueError(msg)
+ self.im_info["srgb"] = s[0]
+ return s
+
+ def chunk_pHYs(self, pos: int, length: int) -> bytes:
+ # pixels per unit
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ if length < 9:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ return s
+ msg = "Truncated pHYs chunk"
+ raise ValueError(msg)
+ px, py = i32(s, 0), i32(s, 4)
+ unit = s[8]
+ if unit == 1: # meter
+ dpi = px * 0.0254, py * 0.0254
+ self.im_info["dpi"] = dpi
+ elif unit == 0:
+ self.im_info["aspect"] = px, py
+ return s
+
+ def chunk_tEXt(self, pos: int, length: int) -> bytes:
+ # text
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ try:
+ k, v = s.split(b"\0", 1)
+ except ValueError:
+ # fallback for broken tEXt tags
+ k = s
+ v = b""
+ if k:
+ k_str = k.decode("latin-1", "strict")
+ v_str = v.decode("latin-1", "replace")
+
+ self.im_info[k_str] = v if k == b"exif" else v_str
+ self.im_text[k_str] = v_str
+ self.check_text_memory(len(v_str))
+
+ return s
+
+ def chunk_zTXt(self, pos: int, length: int) -> bytes:
+ # compressed text
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ try:
+ k, v = s.split(b"\0", 1)
+ except ValueError:
+ k = s
+ v = b""
+ if v:
+ comp_method = v[0]
+ else:
+ comp_method = 0
+ if comp_method != 0:
+ msg = f"Unknown compression method {comp_method} in zTXt chunk"
+ raise SyntaxError(msg)
+ try:
+ v = _safe_zlib_decompress(v[1:])
+ except ValueError:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ v = b""
+ else:
+ raise
+ except zlib.error:
+ v = b""
+
+ if k:
+ k_str = k.decode("latin-1", "strict")
+ v_str = v.decode("latin-1", "replace")
+
+ self.im_info[k_str] = self.im_text[k_str] = v_str
+ self.check_text_memory(len(v_str))
+
+ return s
+
+ def chunk_iTXt(self, pos: int, length: int) -> bytes:
+ # international text
+ assert self.fp is not None
+ r = s = ImageFile._safe_read(self.fp, length)
+ try:
+ k, r = r.split(b"\0", 1)
+ except ValueError:
+ return s
+ if len(r) < 2:
+ return s
+ cf, cm, r = r[0], r[1], r[2:]
+ try:
+ lang, tk, v = r.split(b"\0", 2)
+ except ValueError:
+ return s
+ if cf != 0:
+ if cm == 0:
+ try:
+ v = _safe_zlib_decompress(v)
+ except ValueError:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ return s
+ else:
+ raise
+ except zlib.error:
+ return s
+ else:
+ return s
+ if k == b"XML:com.adobe.xmp":
+ self.im_info["xmp"] = v
+ try:
+ k_str = k.decode("latin-1", "strict")
+ lang_str = lang.decode("utf-8", "strict")
+ tk_str = tk.decode("utf-8", "strict")
+ v_str = v.decode("utf-8", "strict")
+ except UnicodeError:
+ return s
+
+ self.im_info[k_str] = self.im_text[k_str] = iTXt(v_str, lang_str, tk_str)
+ self.check_text_memory(len(v_str))
+
+ return s
+
+ def chunk_eXIf(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ self.im_info["exif"] = b"Exif\x00\x00" + s
+ return s
+
+ # APNG chunks
+ def chunk_acTL(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ if length < 8:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ return s
+ msg = "APNG contains truncated acTL chunk"
+ raise ValueError(msg)
+ if self.im_n_frames is not None:
+ self.im_n_frames = None
+ warnings.warn("Invalid APNG, will use default PNG image if possible")
+ return s
+ n_frames = i32(s)
+ if n_frames == 0 or n_frames > 0x80000000:
+ warnings.warn("Invalid APNG, will use default PNG image if possible")
+ return s
+ self.im_n_frames = n_frames
+ self.im_info["loop"] = i32(s, 4)
+ self.im_custom_mimetype = "image/apng"
+ return s
+
+ def chunk_fcTL(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
+ s = ImageFile._safe_read(self.fp, length)
+ if length < 26:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ return s
+ msg = "APNG contains truncated fcTL chunk"
+ raise ValueError(msg)
+ seq = i32(s)
+ if (self._seq_num is None and seq != 0) or (
+ self._seq_num is not None and self._seq_num != seq - 1
+ ):
+ msg = "APNG contains frame sequence errors"
+ raise SyntaxError(msg)
+ self._seq_num = seq
+ width, height = i32(s, 4), i32(s, 8)
+ px, py = i32(s, 12), i32(s, 16)
+ im_w, im_h = self.im_size
+ if px + width > im_w or py + height > im_h:
+ msg = "APNG contains invalid frames"
+ raise SyntaxError(msg)
+ self.im_info["bbox"] = (px, py, px + width, py + height)
+ delay_num, delay_den = i16(s, 20), i16(s, 22)
+ if delay_den == 0:
+ delay_den = 100
+ self.im_info["duration"] = float(delay_num) / float(delay_den) * 1000
+ self.im_info["disposal"] = s[24]
+ self.im_info["blend"] = s[25]
+ return s
+
+ def chunk_fdAT(self, pos: int, length: int) -> bytes:
+ assert self.fp is not None
+ if length < 4:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ s = ImageFile._safe_read(self.fp, length)
+ return s
+ msg = "APNG contains truncated fDAT chunk"
+ raise ValueError(msg)
+ s = ImageFile._safe_read(self.fp, 4)
+ seq = i32(s)
+ if self._seq_num != seq - 1:
+ msg = "APNG contains frame sequence errors"
+ raise SyntaxError(msg)
+ self._seq_num = seq
+ return self.chunk_IDAT(pos + 4, length - 4)
+
+
+# --------------------------------------------------------------------
+# PNG reader
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(_MAGIC)
+
+
+##
+# Image plugin for PNG images.
+
+
+class PngImageFile(ImageFile.ImageFile):
+ format = "PNG"
+ format_description = "Portable network graphics"
+
+ def _open(self) -> None:
+ assert self.fp is not None
+ if not _accept(self.fp.read(8)):
+ msg = "not a PNG file"
+ raise SyntaxError(msg)
+ self._fp = self.fp
+ self.__frame = 0
+
+ #
+ # Parse headers up to the first IDAT or fDAT chunk
+
+ self.private_chunks: list[tuple[bytes, bytes] | tuple[bytes, bytes, bool]] = []
+ self.png: PngStream | None = PngStream(self.fp)
+
+ while True:
+ #
+ # get next chunk
+
+ cid, pos, length = self.png.read()
+
+ try:
+ s = self.png.call(cid, pos, length)
+ except EOFError:
+ break
+ except AttributeError:
+ logger.debug("%r %s %s (unknown)", cid, pos, length)
+ s = ImageFile._safe_read(self.fp, length)
+ if cid[1:2].islower():
+ self.private_chunks.append((cid, s))
+
+ self.png.crc(cid, s)
+
+ #
+ # Copy relevant attributes from the PngStream. An alternative
+ # would be to let the PngStream class modify these attributes
+ # directly, but that introduces circular references which are
+ # difficult to break if things go wrong in the decoder...
+ # (believe me, I've tried ;-)
+
+ self._mode = self.png.im_mode
+ self._size = self.png.im_size
+ self.info = self.png.im_info
+ self._text: dict[str, str | iTXt] | None = None
+ self.tile = self.png.im_tile
+ self.custom_mimetype = self.png.im_custom_mimetype
+ self.n_frames = self.png.im_n_frames or 1
+ self.default_image = self.info.get("default_image", False)
+
+ if self.png.im_palette:
+ rawmode, data = self.png.im_palette
+ self.palette = ImagePalette.raw(rawmode, data)
+
+ if cid == b"fdAT":
+ self.__prepare_idat = length - 4
+ else:
+ self.__prepare_idat = length # used by load_prepare()
+
+ if self.png.im_n_frames is not None:
+ self._close_exclusive_fp_after_loading = False
+ self.png.save_rewind()
+ self.__rewind_idat = self.__prepare_idat
+ self.__rewind = self._fp.tell()
+ if self.default_image:
+ # IDAT chunk contains default image and not first animation frame
+ self.n_frames += 1
+ self._seek(0)
+ self.is_animated = self.n_frames > 1
+
+ @property
+ def text(self) -> dict[str, str | iTXt]:
+ # experimental
+ if self._text is None:
+ # iTxt, tEXt and zTXt chunks may appear at the end of the file
+ # So load the file to ensure that they are read
+ if self.is_animated:
+ frame = self.__frame
+ # for APNG, seek to the final frame before loading
+ self.seek(self.n_frames - 1)
+ self.load()
+ if self.is_animated:
+ self.seek(frame)
+ assert self._text is not None
+ return self._text
+
+ def verify(self) -> None:
+ """Verify PNG file"""
+
+ if self.fp is None:
+ msg = "verify must be called directly after open"
+ raise RuntimeError(msg)
+
+ # back up to beginning of IDAT block
+ self.fp.seek(self.tile[0][2] - 8)
+
+ assert self.png is not None
+ self.png.verify()
+ self.png.close()
+
+ super().verify()
+
+ def seek(self, frame: int) -> None:
+ if not self._seek_check(frame):
+ return
+ if frame < self.__frame:
+ self._seek(0, True)
+
+ last_frame = self.__frame
+ try:
+ for f in range(self.__frame + 1, frame + 1):
+ self._seek(f)
+ except EOFError as e:
+ self.seek(last_frame)
+ msg = "no more images in APNG file"
+ raise EOFError(msg) from e
+
+ def _seek(self, frame: int, rewind: bool = False) -> None:
+ assert self.png is not None
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
+
+ self.dispose: _imaging.ImagingCore | None
+ dispose_extent = None
+ if frame == 0:
+ if rewind:
+ self._fp.seek(self.__rewind)
+ self.png.rewind()
+ self.__prepare_idat = self.__rewind_idat
+ self._im = None
+ self.info = self.png.im_info
+ self.tile = self.png.im_tile
+ self.fp = self._fp
+ self._prev_im = None
+ self.dispose = None
+ self.default_image = self.info.get("default_image", False)
+ self.dispose_op = self.info.get("disposal")
+ self.blend_op = self.info.get("blend")
+ dispose_extent = self.info.get("bbox")
+ self.__frame = 0
+ else:
+ if frame != self.__frame + 1:
+ msg = f"cannot seek to frame {frame}"
+ raise ValueError(msg)
+
+ # ensure previous frame was loaded
+ self.load()
+
+ if self.dispose:
+ self.im.paste(self.dispose, self.dispose_extent)
+ self._prev_im = self.im.copy()
+
+ self.fp = self._fp
+
+ # advance to the next frame
+ if self.__prepare_idat:
+ ImageFile._safe_read(self.fp, self.__prepare_idat)
+ self.__prepare_idat = 0
+ frame_start = False
+ while True:
+ self.fp.read(4) # CRC
+
+ try:
+ cid, pos, length = self.png.read()
+ except (struct.error, SyntaxError):
+ break
+
+ if cid == b"IEND":
+ msg = "No more images in APNG file"
+ raise EOFError(msg)
+ if cid == b"fcTL":
+ if frame_start:
+ # there must be at least one fdAT chunk between fcTL chunks
+ msg = "APNG missing frame data"
+ raise SyntaxError(msg)
+ frame_start = True
+
+ try:
+ self.png.call(cid, pos, length)
+ except UnicodeDecodeError:
+ break
+ except EOFError:
+ if cid == b"fdAT":
+ length -= 4
+ if frame_start:
+ self.__prepare_idat = length
+ break
+ ImageFile._safe_read(self.fp, length)
+ except AttributeError:
+ logger.debug("%r %s %s (unknown)", cid, pos, length)
+ ImageFile._safe_read(self.fp, length)
+
+ self.__frame = frame
+ self.tile = self.png.im_tile
+ self.dispose_op = self.info.get("disposal")
+ self.blend_op = self.info.get("blend")
+ dispose_extent = self.info.get("bbox")
+
+ if not self.tile:
+ msg = "image not found in APNG frame"
+ raise EOFError(msg)
+ if dispose_extent:
+ self.dispose_extent: tuple[float, float, float, float] = dispose_extent
+
+ # setup frame disposal (actual disposal done when needed in the next _seek())
+ if self._prev_im is None and self.dispose_op == Disposal.OP_PREVIOUS:
+ self.dispose_op = Disposal.OP_BACKGROUND
+
+ self.dispose = None
+ if self.dispose_op == Disposal.OP_PREVIOUS:
+ if self._prev_im:
+ self.dispose = self._prev_im.copy()
+ self.dispose = self._crop(self.dispose, self.dispose_extent)
+ elif self.dispose_op == Disposal.OP_BACKGROUND:
+ self.dispose = Image.core.fill(self.mode, self.size)
+ self.dispose = self._crop(self.dispose, self.dispose_extent)
+
+ def tell(self) -> int:
+ return self.__frame
+
+ def load_prepare(self) -> None:
+ """internal: prepare to read PNG file"""
+
+ if self.info.get("interlace"):
+ self.decoderconfig = self.decoderconfig + (1,)
+
+ self.__idat = self.__prepare_idat # used by load_read()
+ ImageFile.ImageFile.load_prepare(self)
+
+ def load_read(self, read_bytes: int) -> bytes:
+ """internal: read more image data"""
+
+ assert self.png is not None
+ assert self.fp is not None
+ while self.__idat == 0:
+ # end of chunk, skip forward to next one
+
+ self.fp.read(4) # CRC
+
+ cid, pos, length = self.png.read()
+
+ if cid not in [b"IDAT", b"DDAT", b"fdAT"]:
+ self.png.push(cid, pos, length)
+ return b""
+
+ if cid == b"fdAT":
+ try:
+ self.png.call(cid, pos, length)
+ except EOFError:
+ pass
+ self.__idat = length - 4 # sequence_num has already been read
+ else:
+ self.__idat = length # empty chunks are allowed
+
+ # read more data from this chunk
+ if read_bytes <= 0:
+ read_bytes = self.__idat
+ else:
+ read_bytes = min(read_bytes, self.__idat)
+
+ self.__idat = self.__idat - read_bytes
+
+ return self.fp.read(read_bytes)
+
+ def load_end(self) -> None:
+ """internal: finished reading image data"""
+ assert self.png is not None
+ assert self.fp is not None
+ if self.__idat != 0:
+ self.fp.read(self.__idat)
+ while True:
+ self.fp.read(4) # CRC
+
+ try:
+ cid, pos, length = self.png.read()
+ except (struct.error, SyntaxError):
+ break
+
+ if cid == b"IEND":
+ break
+ elif cid == b"fcTL" and self.is_animated:
+ # start of the next frame, stop reading
+ self.__prepare_idat = 0
+ self.png.push(cid, pos, length)
+ break
+
+ try:
+ self.png.call(cid, pos, length)
+ except UnicodeDecodeError:
+ break
+ except EOFError:
+ if cid == b"fdAT":
+ length -= 4
+ try:
+ ImageFile._safe_read(self.fp, length)
+ except OSError as e:
+ if ImageFile.LOAD_TRUNCATED_IMAGES:
+ break
+ else:
+ raise e
+ except AttributeError:
+ logger.debug("%r %s %s (unknown)", cid, pos, length)
+ s = ImageFile._safe_read(self.fp, length)
+ if cid[1:2].islower():
+ self.private_chunks.append((cid, s, True))
+ self._text = self.png.im_text
+ if not self.is_animated:
+ self.png.close()
+ self.png = None
+ else:
+ if self._prev_im and self.blend_op == Blend.OP_OVER:
+ updated = self._crop(self.im, self.dispose_extent)
+ if self.im.mode == "RGB" and "transparency" in self.info:
+ mask = updated.convert_transparent(
+ "RGBA", self.info["transparency"]
+ )
+ else:
+ if self.im.mode == "P" and "transparency" in self.info:
+ t = self.info["transparency"]
+ if isinstance(t, bytes):
+ updated.putpalettealphas(t)
+ elif isinstance(t, int):
+ updated.putpalettealpha(t)
+ mask = updated.convert("RGBA")
+ self._prev_im.paste(updated, self.dispose_extent, mask)
+ self.im = self._prev_im
+
+ def _getexif(self) -> dict[int, Any] | None:
+ if "exif" not in self.info:
+ self.load()
+ if "exif" not in self.info and "Raw profile type exif" not in self.info:
+ return None
+ return self.getexif()._get_merged_dict()
+
+ def getexif(self) -> Image.Exif:
+ if "exif" not in self.info:
+ self.load()
+
+ return super().getexif()
+
+
+# --------------------------------------------------------------------
+# PNG writer
+
+_OUTMODES = {
+ # supported PIL modes, and corresponding rawmode, bit depth and color type
+ "1": ("1", b"\x01", b"\x00"),
+ "L;1": ("L;1", b"\x01", b"\x00"),
+ "L;2": ("L;2", b"\x02", b"\x00"),
+ "L;4": ("L;4", b"\x04", b"\x00"),
+ "L": ("L", b"\x08", b"\x00"),
+ "LA": ("LA", b"\x08", b"\x04"),
+ "I": ("I;16B", b"\x10", b"\x00"),
+ "I;16": ("I;16B", b"\x10", b"\x00"),
+ "I;16B": ("I;16B", b"\x10", b"\x00"),
+ "P;1": ("P;1", b"\x01", b"\x03"),
+ "P;2": ("P;2", b"\x02", b"\x03"),
+ "P;4": ("P;4", b"\x04", b"\x03"),
+ "P": ("P", b"\x08", b"\x03"),
+ "RGB": ("RGB", b"\x08", b"\x02"),
+ "RGBA": ("RGBA", b"\x08", b"\x06"),
+}
+
+
+def putchunk(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
+ """Write a PNG chunk (including CRC field)"""
+
+ byte_data = b"".join(data)
+
+ fp.write(o32(len(byte_data)) + cid)
+ fp.write(byte_data)
+ crc = _crc32(byte_data, _crc32(cid))
+ fp.write(o32(crc))
+
+
+class _idat:
+ # wrap output from the encoder in IDAT chunks
+
+ def __init__(self, fp: IO[bytes], chunk: Callable[..., None]) -> None:
+ self.fp = fp
+ self.chunk = chunk
+
+ def write(self, data: bytes) -> None:
+ self.chunk(self.fp, b"IDAT", data)
+
+
+class _fdat:
+ # wrap encoder output in fdAT chunks
+
+ def __init__(self, fp: IO[bytes], chunk: Callable[..., None], seq_num: int) -> None:
+ self.fp = fp
+ self.chunk = chunk
+ self.seq_num = seq_num
+
+ def write(self, data: bytes) -> None:
+ self.chunk(self.fp, b"fdAT", o32(self.seq_num), data)
+ self.seq_num += 1
+
+
+def _apply_encoderinfo(im: Image.Image, encoderinfo: dict[str, Any]) -> None:
+ im.encoderconfig = (
+ encoderinfo.get("optimize", False),
+ encoderinfo.get("compress_level", -1),
+ encoderinfo.get("compress_type", -1),
+ encoderinfo.get("dictionary", b""),
+ )
+
+
+class _Frame(NamedTuple):
+ im: Image.Image
+ bbox: tuple[int, int, int, int] | None
+ encoderinfo: dict[str, Any]
+
+
+def _write_multiple_frames(
+ im: Image.Image,
+ fp: IO[bytes],
+ chunk: Callable[..., None],
+ mode: str,
+ rawmode: str,
+ default_image: Image.Image | None,
+ append_images: list[Image.Image],
+) -> Image.Image | None:
+ duration = im.encoderinfo.get("duration")
+ loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
+ disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
+ blend = im.encoderinfo.get("blend", im.info.get("blend", Blend.OP_SOURCE))
+
+ if default_image:
+ chain = itertools.chain(append_images)
+ else:
+ chain = itertools.chain([im], append_images)
+
+ im_frames: list[_Frame] = []
+ frame_count = 0
+ for im_seq in chain:
+ for im_frame in ImageSequence.Iterator(im_seq):
+ if im_frame.mode == mode:
+ im_frame = im_frame.copy()
+ else:
+ im_frame = im_frame.convert(mode)
+ encoderinfo = im.encoderinfo.copy()
+ if isinstance(duration, (list, tuple)):
+ encoderinfo["duration"] = duration[frame_count]
+ elif duration is None and "duration" in im_frame.info:
+ encoderinfo["duration"] = im_frame.info["duration"]
+ if isinstance(disposal, (list, tuple)):
+ encoderinfo["disposal"] = disposal[frame_count]
+ if isinstance(blend, (list, tuple)):
+ encoderinfo["blend"] = blend[frame_count]
+ frame_count += 1
+
+ if im_frames:
+ previous = im_frames[-1]
+ prev_disposal = previous.encoderinfo.get("disposal")
+ prev_blend = previous.encoderinfo.get("blend")
+ if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
+ prev_disposal = Disposal.OP_BACKGROUND
+
+ if prev_disposal == Disposal.OP_BACKGROUND:
+ base_im = previous.im.copy()
+ dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
+ bbox = previous.bbox
+ if bbox:
+ dispose = dispose.crop(bbox)
+ else:
+ bbox = (0, 0) + im.size
+ base_im.paste(dispose, bbox)
+ elif prev_disposal == Disposal.OP_PREVIOUS:
+ base_im = im_frames[-2].im
+ else:
+ base_im = previous.im
+ delta = ImageChops.subtract_modulo(
+ im_frame.convert("RGBA"), base_im.convert("RGBA")
+ )
+ bbox = delta.getbbox(alpha_only=False)
+ if (
+ not bbox
+ and prev_disposal == encoderinfo.get("disposal")
+ and prev_blend == encoderinfo.get("blend")
+ and "duration" in encoderinfo
+ ):
+ previous.encoderinfo["duration"] += encoderinfo["duration"]
+ continue
+ else:
+ bbox = None
+ im_frames.append(_Frame(im_frame, bbox, encoderinfo))
+
+ if len(im_frames) == 1 and not default_image:
+ return im_frames[0].im
+
+ # animation control
+ chunk(
+ fp,
+ b"acTL",
+ o32(len(im_frames)), # 0: num_frames
+ o32(loop), # 4: num_plays
+ )
+
+ # default image IDAT (if it exists)
+ if default_image:
+ default_im = im if im.mode == mode else im.convert(mode)
+ _apply_encoderinfo(default_im, im.encoderinfo)
+ ImageFile._save(
+ default_im,
+ cast(IO[bytes], _idat(fp, chunk)),
+ [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)],
+ )
+
+ seq_num = 0
+ for frame, frame_data in enumerate(im_frames):
+ im_frame = frame_data.im
+ if not frame_data.bbox:
+ bbox = (0, 0) + im_frame.size
+ else:
+ bbox = frame_data.bbox
+ im_frame = im_frame.crop(bbox)
+ size = im_frame.size
+ encoderinfo = frame_data.encoderinfo
+ frame_duration = encoderinfo.get("duration", 0)
+ delay = Fraction(frame_duration / 1000).limit_denominator(65535)
+ if delay.numerator > 65535:
+ msg = "cannot write duration"
+ raise ValueError(msg)
+ frame_disposal = encoderinfo.get("disposal", disposal)
+ frame_blend = encoderinfo.get("blend", blend)
+ # frame control
+ chunk(
+ fp,
+ b"fcTL",
+ o32(seq_num), # sequence_number
+ o32(size[0]), # width
+ o32(size[1]), # height
+ o32(bbox[0]), # x_offset
+ o32(bbox[1]), # y_offset
+ o16(delay.numerator), # delay_numerator
+ o16(delay.denominator), # delay_denominator
+ o8(frame_disposal), # dispose_op
+ o8(frame_blend), # blend_op
+ )
+ seq_num += 1
+ # frame data
+ _apply_encoderinfo(im_frame, im.encoderinfo)
+ if frame == 0 and not default_image:
+ # first frame must be in IDAT chunks for backwards compatibility
+ ImageFile._save(
+ im_frame,
+ cast(IO[bytes], _idat(fp, chunk)),
+ [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
+ )
+ else:
+ fdat_chunks = _fdat(fp, chunk, seq_num)
+ ImageFile._save(
+ im_frame,
+ cast(IO[bytes], fdat_chunks),
+ [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)],
+ )
+ seq_num = fdat_chunks.seq_num
+ return None
+
+
+def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ _save(im, fp, filename, save_all=True)
+
+
+def _save(
+ im: Image.Image,
+ fp: IO[bytes],
+ filename: str | bytes,
+ chunk: Callable[..., None] = putchunk,
+ save_all: bool = False,
+) -> None:
+ # save an image to disk (called by the save method)
+
+ if save_all:
+ default_image = im.encoderinfo.get(
+ "default_image", im.info.get("default_image")
+ )
+ modes = set()
+ sizes = set()
+ append_images = im.encoderinfo.get("append_images", [])
+ for im_seq in itertools.chain([im], append_images):
+ for im_frame in ImageSequence.Iterator(im_seq):
+ modes.add(im_frame.mode)
+ sizes.add(im_frame.size)
+ for mode in ("RGBA", "RGB", "P"):
+ if mode in modes:
+ break
+ else:
+ mode = modes.pop()
+ size = tuple(max(frame_size[i] for frame_size in sizes) for i in range(2))
+ else:
+ size = im.size
+ mode = im.mode
+
+ outmode = mode
+ palette = []
+ if im.palette:
+ palette = im.getpalette() or []
+ if mode == "P":
+ #
+ # attempt to minimize storage requirements for palette images
+ if "bits" in im.encoderinfo:
+ # number of bits specified by user
+ colors = min(1 << im.encoderinfo["bits"], 256)
+ else:
+ # check palette contents
+ if im.palette:
+ colors = max(min(len(palette) // 3, 256), 1)
+ else:
+ colors = 256
+
+ if colors <= 16:
+ if colors <= 2:
+ bits = 1
+ elif colors <= 4:
+ bits = 2
+ else:
+ bits = 4
+ outmode += f";{bits}"
+
+ # get the corresponding PNG mode
+ try:
+ rawmode, bit_depth, color_type = _OUTMODES[outmode]
+ except KeyError as e:
+ msg = f"cannot write mode {mode} as PNG"
+ raise OSError(msg) from e
+ if outmode == "I":
+ deprecate("Saving I mode images as PNG", 13, stacklevel=4)
+
+ #
+ # write minimal PNG file
+
+ fp.write(_MAGIC)
+
+ chunk(
+ fp,
+ b"IHDR",
+ o32(size[0]), # 0: size
+ o32(size[1]),
+ bit_depth,
+ color_type,
+ b"\0", # 10: compression
+ b"\0", # 11: filter category
+ b"\0", # 12: interlace flag
+ )
+
+ chunks = [b"cHRM", b"cICP", b"gAMA", b"sBIT", b"sRGB", b"tIME"]
+
+ if icc := im.encoderinfo.get("icc_profile", im.info.get("icc_profile")):
+ # ICC profile
+ # according to PNG spec, the iCCP chunk contains:
+ # Profile name 1-79 bytes (character string)
+ # Null separator 1 byte (null character)
+ # Compression method 1 byte (0)
+ # Compressed profile n bytes (zlib with deflate compression)
+ name = b"ICC Profile"
+ data = name + b"\0\0" + zlib.compress(icc)
+ chunk(fp, b"iCCP", data)
+
+ # You must either have sRGB or iCCP.
+ # Disallow sRGB chunks when an iCCP-chunk has been emitted.
+ chunks.remove(b"sRGB")
+
+ if info := im.encoderinfo.get("pnginfo"):
+ chunks_multiple_allowed = [b"sPLT", b"iTXt", b"tEXt", b"zTXt"]
+ for info_chunk in info.chunks:
+ cid, data = info_chunk[:2]
+ if cid in chunks:
+ chunks.remove(cid)
+ chunk(fp, cid, data)
+ elif cid in chunks_multiple_allowed:
+ chunk(fp, cid, data)
+ elif cid[1:2].islower():
+ # Private chunk
+ after_idat = len(info_chunk) == 3 and info_chunk[2]
+ if not after_idat:
+ chunk(fp, cid, data)
+
+ if im.mode == "P":
+ palette_byte_number = colors * 3
+ palette_bytes = bytes(palette[:palette_byte_number])
+ while len(palette_bytes) < palette_byte_number:
+ palette_bytes += b"\0"
+ chunk(fp, b"PLTE", palette_bytes)
+
+ transparency = im.encoderinfo.get("transparency", im.info.get("transparency"))
+
+ if transparency is not None:
+ if im.mode == "P":
+ # limit to actual palette size
+ alpha_bytes = colors
+ if isinstance(transparency, bytes):
+ chunk(fp, b"tRNS", transparency[:alpha_bytes])
+ elif isinstance(transparency, int):
+ transparency = max(0, min(255, transparency))
+ alpha = b"\xff" * transparency + b"\0"
+ chunk(fp, b"tRNS", alpha[:alpha_bytes])
+ else:
+ msg = "transparency for P must be an integer or bytes"
+ raise ValueError(msg)
+ elif im.mode in ("1", "L", "I", "I;16"):
+ if isinstance(transparency, int):
+ transparency = max(0, min(65535, transparency))
+ chunk(fp, b"tRNS", o16(transparency))
+ else:
+ msg = f"transparency for {im.mode} must be an integer"
+ raise ValueError(msg)
+ elif im.mode == "RGB":
+ if not isinstance(transparency, (list, tuple)):
+ msg = "transparency for RGB must be list or tuple"
+ raise ValueError(msg)
+ elif len(transparency) != 3:
+ msg = "transparency for RGB must have length 3"
+ raise ValueError(msg)
+ else:
+ red, green, blue = transparency
+ chunk(fp, b"tRNS", o16(red) + o16(green) + o16(blue))
+ elif im.encoderinfo.get("transparency") is not None:
+ # don't bother with transparency if it's an RGBA
+ # and it's in the info dict. It's probably just stale.
+ msg = "cannot use transparency for this mode"
+ raise OSError(msg)
+ elif im.mode == "P" and im.im.getpalettemode() == "RGBA":
+ alpha = im.im.getpalette("RGBA", "A")
+ alpha_bytes = colors
+ chunk(fp, b"tRNS", alpha[:alpha_bytes])
+
+ if dpi := im.encoderinfo.get("dpi"):
+ chunk(
+ fp,
+ b"pHYs",
+ o32(int(dpi[0] / 0.0254 + 0.5)),
+ o32(int(dpi[1] / 0.0254 + 0.5)),
+ b"\x01",
+ )
+
+ if info:
+ chunks = [b"bKGD", b"hIST"]
+ for info_chunk in info.chunks:
+ cid, data = info_chunk[:2]
+ if cid in chunks:
+ chunks.remove(cid)
+ chunk(fp, cid, data)
+
+ if exif := im.encoderinfo.get("exif"):
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes(8)
+ if exif.startswith(b"Exif\x00\x00"):
+ exif = exif[6:]
+ chunk(fp, b"eXIf", exif)
+
+ single_im: Image.Image | None = im
+ if save_all:
+ single_im = _write_multiple_frames(
+ im, fp, chunk, mode, rawmode, default_image, append_images
+ )
+ if single_im:
+ _apply_encoderinfo(single_im, im.encoderinfo)
+ ImageFile._save(
+ single_im,
+ cast(IO[bytes], _idat(fp, chunk)),
+ [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)],
+ )
+
+ if info:
+ for info_chunk in info.chunks:
+ cid, data = info_chunk[:2]
+ if cid[1:2].islower():
+ # Private chunk
+ after_idat = len(info_chunk) == 3 and info_chunk[2]
+ if after_idat:
+ chunk(fp, cid, data)
+
+ chunk(fp, b"IEND", b"")
+
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+# --------------------------------------------------------------------
+# PNG chunk converter
+
+
+def getchunks(im: Image.Image, **params: Any) -> list[tuple[bytes, bytes, bytes]]:
+ """Return a list of PNG chunks representing this image."""
+ from io import BytesIO
+
+ chunks = []
+
+ def append(fp: IO[bytes], cid: bytes, *data: bytes) -> None:
+ byte_data = b"".join(data)
+ crc = o32(_crc32(byte_data, _crc32(cid)))
+ chunks.append((cid, byte_data, crc))
+
+ fp = BytesIO()
+
+ try:
+ im.encoderinfo = params
+ _save(im, fp, "", append)
+ finally:
+ del im.encoderinfo
+
+ return chunks
+
+
+# --------------------------------------------------------------------
+# Registry
+
+Image.register_open(PngImageFile.format, PngImageFile, _accept)
+Image.register_save(PngImageFile.format, _save)
+Image.register_save_all(PngImageFile.format, _save_all)
+
+Image.register_extensions(PngImageFile.format, [".png", ".apng"])
+
+Image.register_mime(PngImageFile.format, "image/png")
diff --git a/venv/lib/python3.11/site-packages/PIL/PpmImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PpmImagePlugin.py
new file mode 100644
index 000000000..ca6093385
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PpmImagePlugin.py
@@ -0,0 +1,375 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# PPM support for PIL
+#
+# History:
+# 96-03-24 fl Created
+# 98-03-06 fl Write RGBA images (as RGB, that is)
+#
+# Copyright (c) Secret Labs AB 1997-98.
+# Copyright (c) Fredrik Lundh 1996.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import math
+from typing import IO
+
+from . import Image, ImageFile
+from ._binary import i16be as i16
+from ._binary import o8
+from ._binary import o32le as o32
+
+#
+# --------------------------------------------------------------------
+
+b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d"
+
+MODES = {
+ # standard
+ b"P1": "1",
+ b"P2": "L",
+ b"P3": "RGB",
+ b"P4": "1",
+ b"P5": "L",
+ b"P6": "RGB",
+ # extensions
+ b"P0CMYK": "CMYK",
+ b"Pf": "F",
+ # PIL extensions (for test purposes only)
+ b"PyP": "P",
+ b"PyRGBA": "RGBA",
+ b"PyCMYK": "CMYK",
+}
+
+
+def _accept(prefix: bytes) -> bool:
+ return len(prefix) >= 2 and prefix.startswith(b"P") and prefix[1] in b"0123456fy"
+
+
+##
+# Image plugin for PBM, PGM, and PPM images.
+
+
+class PpmImageFile(ImageFile.ImageFile):
+ format = "PPM"
+ format_description = "Pbmplus image"
+
+ def _read_magic(self) -> bytes:
+ assert self.fp is not None
+
+ magic = b""
+ # read until whitespace or longest available magic number
+ for _ in range(6):
+ c = self.fp.read(1)
+ if not c or c in b_whitespace:
+ break
+ magic += c
+ return magic
+
+ def _read_token(self) -> bytes:
+ assert self.fp is not None
+
+ token = b""
+ while len(token) <= 10: # read until next whitespace or limit of 10 characters
+ c = self.fp.read(1)
+ if not c:
+ break
+ elif c in b_whitespace: # token ended
+ if not token:
+ # skip whitespace at start
+ continue
+ break
+ elif c == b"#":
+ # ignores rest of the line; stops at CR, LF or EOF
+ while self.fp.read(1) not in b"\r\n":
+ pass
+ continue
+ token += c
+ if not token:
+ # Token was not even 1 byte
+ msg = "Reached EOF while reading header"
+ raise ValueError(msg)
+ elif len(token) > 10:
+ msg_too_long = b"Token too long in file header: %s" % token
+ raise ValueError(msg_too_long)
+ return token
+
+ def _open(self) -> None:
+ assert self.fp is not None
+
+ magic_number = self._read_magic()
+ try:
+ mode = MODES[magic_number]
+ except KeyError:
+ msg = "not a PPM file"
+ raise SyntaxError(msg)
+ self._mode = mode
+
+ if magic_number in (b"P1", b"P4"):
+ self.custom_mimetype = "image/x-portable-bitmap"
+ elif magic_number in (b"P2", b"P5"):
+ self.custom_mimetype = "image/x-portable-graymap"
+ elif magic_number in (b"P3", b"P6"):
+ self.custom_mimetype = "image/x-portable-pixmap"
+
+ self._size = int(self._read_token()), int(self._read_token())
+
+ decoder_name = "raw"
+ if magic_number in (b"P1", b"P2", b"P3"):
+ decoder_name = "ppm_plain"
+
+ args: str | tuple[str | int, ...]
+ if mode == "1":
+ args = "1;I"
+ elif mode == "F":
+ scale = float(self._read_token())
+ if scale == 0.0 or not math.isfinite(scale):
+ msg = "scale must be finite and non-zero"
+ raise ValueError(msg)
+ self.info["scale"] = abs(scale)
+
+ rawmode = "F;32F" if scale < 0 else "F;32BF"
+ args = (rawmode, 0, -1)
+ else:
+ maxval = int(self._read_token())
+ if not 0 < maxval < 65536:
+ msg = "maxval must be greater than 0 and less than 65536"
+ raise ValueError(msg)
+ if maxval > 255 and mode == "L":
+ self._mode = "I"
+
+ rawmode = mode
+ if decoder_name != "ppm_plain":
+ # If maxval matches a bit depth, use the raw decoder directly
+ if maxval == 65535 and mode == "L":
+ rawmode = "I;16B"
+ elif maxval != 255:
+ decoder_name = "ppm"
+
+ args = rawmode if decoder_name == "raw" else (rawmode, maxval)
+ self.tile = [
+ ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args)
+ ]
+
+
+#
+# --------------------------------------------------------------------
+
+
+class PpmPlainDecoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+ _comment_spans: bool
+
+ def _read_block(self) -> bytes:
+ assert self.fd is not None
+
+ return self.fd.read(ImageFile.SAFEBLOCK)
+
+ def _find_comment_end(self, block: bytes, start: int = 0) -> int:
+ a = block.find(b"\n", start)
+ b = block.find(b"\r", start)
+ return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1)
+
+ def _ignore_comments(self, block: bytes) -> bytes:
+ if self._comment_spans:
+ # Finish current comment
+ while block:
+ comment_end = self._find_comment_end(block)
+ if comment_end != -1:
+ # Comment ends in this block
+ # Delete tail of comment
+ block = block[comment_end + 1 :]
+ break
+ else:
+ # Comment spans whole block
+ # So read the next block, looking for the end
+ block = self._read_block()
+
+ # Search for any further comments
+ self._comment_spans = False
+ while True:
+ comment_start = block.find(b"#")
+ if comment_start == -1:
+ # No comment found
+ break
+ comment_end = self._find_comment_end(block, comment_start)
+ if comment_end != -1:
+ # Comment ends in this block
+ # Delete comment
+ block = block[:comment_start] + block[comment_end + 1 :]
+ else:
+ # Comment continues to next block(s)
+ block = block[:comment_start]
+ self._comment_spans = True
+ break
+ return block
+
+ def _decode_bitonal(self) -> bytearray:
+ """
+ This is a separate method because in the plain PBM format, all data tokens are
+ exactly one byte, so the inter-token whitespace is optional.
+ """
+ data = bytearray()
+ total_bytes = self.state.xsize * self.state.ysize
+
+ while len(data) != total_bytes:
+ block = self._read_block() # read next block
+ if not block:
+ # eof
+ break
+
+ block = self._ignore_comments(block)
+
+ tokens = b"".join(block.split())
+ for token in tokens:
+ if token not in (48, 49):
+ msg = b"Invalid token for this mode: %s" % bytes([token])
+ raise ValueError(msg)
+ data = (data + tokens)[:total_bytes]
+ invert = bytes.maketrans(b"01", b"\xff\x00")
+ return data.translate(invert)
+
+ def _decode_blocks(self, maxval: int) -> bytearray:
+ data = bytearray()
+ max_len = 10
+ out_byte_count = 4 if self.mode == "I" else 1
+ out_max = 65535 if self.mode == "I" else 255
+ bands = Image.getmodebands(self.mode)
+ total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
+
+ half_token = b""
+ while len(data) != total_bytes:
+ block = self._read_block() # read next block
+ if not block:
+ if half_token:
+ block = b" " # flush half_token
+ else:
+ # eof
+ break
+
+ block = self._ignore_comments(block)
+
+ if half_token:
+ block = half_token + block # stitch half_token to new block
+ half_token = b""
+
+ tokens = block.split()
+
+ if block and not block[-1:].isspace(): # block might split token
+ half_token = tokens.pop() # save half token for later
+ if len(half_token) > max_len: # prevent buildup of half_token
+ msg = (
+ b"Token too long found in data: %s" % half_token[: max_len + 1]
+ )
+ raise ValueError(msg)
+
+ for token in tokens:
+ if len(token) > max_len:
+ msg = b"Token too long found in data: %s" % token[: max_len + 1]
+ raise ValueError(msg)
+ value = int(token)
+ if value < 0:
+ msg_str = f"Channel value is negative: {value}"
+ raise ValueError(msg_str)
+ if value > maxval:
+ msg_str = f"Channel value too large for this mode: {value}"
+ raise ValueError(msg_str)
+ value = round(value / maxval * out_max)
+ data += o32(value) if self.mode == "I" else o8(value)
+ if len(data) == total_bytes: # finished!
+ break
+ return data
+
+ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
+ self._comment_spans = False
+ if self.mode == "1":
+ data = self._decode_bitonal()
+ rawmode = "1;8"
+ else:
+ maxval = self.args[-1]
+ data = self._decode_blocks(maxval)
+ rawmode = "I;32" if self.mode == "I" else self.mode
+ self.set_as_raw(bytes(data), rawmode)
+ return -1, 0
+
+
+class PpmDecoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+
+ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
+ assert self.fd is not None
+
+ data = bytearray()
+ maxval = self.args[-1]
+ in_byte_count = 1 if maxval < 256 else 2
+ out_byte_count = 4 if self.mode == "I" else 1
+ out_max = 65535 if self.mode == "I" else 255
+ bands = Image.getmodebands(self.mode)
+ dest_length = self.state.xsize * self.state.ysize * bands * out_byte_count
+ while len(data) < dest_length:
+ pixels = self.fd.read(in_byte_count * bands)
+ if len(pixels) < in_byte_count * bands:
+ # eof
+ break
+ for b in range(bands):
+ value = (
+ pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)
+ )
+ value = min(out_max, round(value / maxval * out_max))
+ data += o32(value) if self.mode == "I" else o8(value)
+ rawmode = "I;32" if self.mode == "I" else self.mode
+ self.set_as_raw(bytes(data), rawmode)
+ return -1, 0
+
+
+#
+# --------------------------------------------------------------------
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode == "1":
+ rawmode, head = "1;I", b"P4"
+ elif im.mode == "L":
+ rawmode, head = "L", b"P5"
+ elif im.mode in ("I", "I;16"):
+ rawmode, head = "I;16B", b"P5"
+ elif im.mode in ("RGB", "RGBA"):
+ rawmode, head = "RGB", b"P6"
+ elif im.mode == "F":
+ rawmode, head = "F;32F", b"Pf"
+ else:
+ msg = f"cannot write mode {im.mode} as PPM"
+ raise OSError(msg)
+ fp.write(head + b"\n%d %d\n" % im.size)
+ if head == b"P6":
+ fp.write(b"255\n")
+ elif head == b"P5":
+ if rawmode == "L":
+ fp.write(b"255\n")
+ else:
+ fp.write(b"65535\n")
+ elif head == b"Pf":
+ fp.write(b"-1.0\n")
+ row_order = -1 if im.mode == "F" else 1
+ ImageFile._save(
+ im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]
+ )
+
+
+#
+# --------------------------------------------------------------------
+
+
+Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
+Image.register_save(PpmImageFile.format, _save)
+
+Image.register_decoder("ppm", PpmDecoder)
+Image.register_decoder("ppm_plain", PpmPlainDecoder)
+
+Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])
+
+Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")
diff --git a/venv/lib/python3.11/site-packages/PIL/PsdImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/PsdImagePlugin.py
new file mode 100644
index 000000000..201909abd
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/PsdImagePlugin.py
@@ -0,0 +1,339 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# Adobe PSD 2.5/3.0 file handling
+#
+# History:
+# 1995-09-01 fl Created
+# 1997-01-03 fl Read most PSD images
+# 1997-01-18 fl Fixed P and CMYK support
+# 2001-10-21 fl Added seek/tell support (for layers)
+#
+# Copyright (c) 1997-2001 by Secret Labs AB.
+# Copyright (c) 1995-2001 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+from functools import cached_property
+from typing import IO
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import i8
+from ._binary import i16be as i16
+from ._binary import i32be as i32
+from ._binary import si16be as si16
+from ._binary import si32be as si32
+from ._util import DeferredError
+
+MODES = {
+ # (photoshop mode, bits) -> (pil mode, required channels)
+ (0, 1): ("1", 1),
+ (0, 8): ("L", 1),
+ (1, 8): ("L", 1),
+ (2, 8): ("P", 1),
+ (3, 8): ("RGB", 3),
+ (4, 8): ("CMYK", 4),
+ (7, 8): ("L", 1), # FIXME: multilayer
+ (8, 8): ("L", 1), # duotone
+ (9, 8): ("LAB", 3),
+}
+
+
+# --------------------------------------------------------------------.
+# read PSD images
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(b"8BPS")
+
+
+##
+# Image plugin for Photoshop images.
+
+
+class PsdImageFile(ImageFile.ImageFile):
+ format = "PSD"
+ format_description = "Adobe Photoshop"
+ _close_exclusive_fp_after_loading = False
+
+ def _open(self) -> None:
+ assert self.fp is not None
+ read = self.fp.read
+
+ #
+ # header
+
+ s = read(26)
+ if not _accept(s) or i16(s, 4) != 1:
+ msg = "not a PSD file"
+ raise SyntaxError(msg)
+
+ psd_bits = i16(s, 22)
+ psd_channels = i16(s, 12)
+ psd_mode = i16(s, 24)
+
+ mode, channels = MODES[(psd_mode, psd_bits)]
+
+ if channels > psd_channels:
+ msg = "not enough channels"
+ raise OSError(msg)
+ if mode == "RGB" and psd_channels == 4:
+ mode = "RGBA"
+ channels = 4
+
+ self._mode = mode
+ self._size = i32(s, 18), i32(s, 14)
+
+ #
+ # color mode data
+
+ size = i32(read(4))
+ if size:
+ data = read(size)
+ if mode == "P" and size == 768:
+ self.palette = ImagePalette.raw("RGB;L", data)
+
+ #
+ # image resources
+
+ self.resources = []
+
+ size = i32(read(4))
+ if size:
+ # load resources
+ end = self.fp.tell() + size
+ while self.fp.tell() < end:
+ read(4) # signature
+ id = i16(read(2))
+ name = read(i8(read(1)))
+ if not (len(name) & 1):
+ read(1) # padding
+ data = read(i32(read(4)))
+ if len(data) & 1:
+ read(1) # padding
+ self.resources.append((id, name, data))
+ if id == 1039: # ICC profile
+ self.info["icc_profile"] = data
+
+ #
+ # layer and mask information
+
+ self._layers_position = None
+
+ size = i32(read(4))
+ if size:
+ end = self.fp.tell() + size
+ size = i32(read(4))
+ if size:
+ self._layers_position = self.fp.tell()
+ self._layers_size = size
+ self.fp.seek(end)
+ self._n_frames: int | None = None
+
+ #
+ # image descriptor
+
+ self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels)
+
+ # keep the file open
+ self._fp = self.fp
+ self.frame = 1
+ self._min_frame = 1
+
+ @cached_property
+ def layers(
+ self,
+ ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
+ layers = []
+ if self._layers_position is not None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
+ self._fp.seek(self._layers_position)
+ _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size))
+ layers = _layerinfo(_layer_data, self._layers_size)
+ self._n_frames = len(layers)
+ return layers
+
+ @property
+ def n_frames(self) -> int:
+ if self._n_frames is None:
+ self._n_frames = len(self.layers)
+ return self._n_frames
+
+ @property
+ def is_animated(self) -> bool:
+ return len(self.layers) > 1
+
+ def seek(self, layer: int) -> None:
+ if not self._seek_check(layer):
+ return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
+
+ # seek to given layer (1..max)
+ if layer > len(self.layers):
+ msg = "no more images in PSD file"
+ raise EOFError(msg)
+ _, mode, _, tile = self.layers[layer - 1]
+ self._mode = mode
+ self.tile = tile
+ self.frame = layer
+ self.fp = self._fp
+
+ def tell(self) -> int:
+ # return layer number (0=image, 1..max=layers)
+ return self.frame
+
+
+def _layerinfo(
+ fp: IO[bytes], ct_bytes: int
+) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]:
+ # read layerinfo block
+ layers = []
+
+ def read(size: int) -> bytes:
+ return ImageFile._safe_read(fp, size)
+
+ ct = si16(read(2))
+
+ # sanity check
+ if ct_bytes < (abs(ct) * 20):
+ msg = "Layer block too short for number of layers requested"
+ raise SyntaxError(msg)
+
+ for _ in range(abs(ct)):
+ # bounding box
+ y0 = si32(read(4))
+ x0 = si32(read(4))
+ y1 = si32(read(4))
+ x1 = si32(read(4))
+
+ # image info
+ bands = []
+ ct_types = i16(read(2))
+ if ct_types > 4:
+ fp.seek(ct_types * 6 + 12, io.SEEK_CUR)
+ size = i32(read(4))
+ fp.seek(size, io.SEEK_CUR)
+ continue
+
+ for _ in range(ct_types):
+ channel_id = i16(read(2))
+
+ if channel_id == 65535:
+ b = "A"
+ elif channel_id < 4:
+ b = "RGBA"[channel_id]
+ else:
+ b = ""
+
+ bands.append(b)
+ read(4) # size
+
+ # figure out the image mode
+ bands.sort()
+ if bands == ["R"]:
+ mode = "L"
+ elif bands == ["B", "G", "R"]:
+ mode = "RGB"
+ elif bands == ["A", "B", "G", "R"]:
+ mode = "RGBA"
+ else:
+ mode = "" # unknown
+
+ # skip over blend flags and extra information
+ read(12) # filler
+ name = ""
+ size = i32(read(4)) # length of the extra data field
+ if size:
+ data_end = fp.tell() + size
+
+ length = i32(read(4))
+ if length:
+ fp.seek(length - 16, io.SEEK_CUR)
+
+ length = i32(read(4))
+ if length:
+ fp.seek(length, io.SEEK_CUR)
+
+ length = i8(read(1))
+ if length:
+ # Don't know the proper encoding,
+ # Latin-1 should be a good guess
+ name = read(length).decode("latin-1", "replace")
+
+ fp.seek(data_end)
+ layers.append((name, mode, (x0, y0, x1, y1)))
+
+ # get tiles
+ layerinfo = []
+ for i, (name, mode, bbox) in enumerate(layers):
+ tile = []
+ for m in mode:
+ t = _maketile(fp, m, bbox, 1)
+ if t:
+ tile.extend(t)
+ layerinfo.append((name, mode, bbox, tile))
+
+ return layerinfo
+
+
+def _maketile(
+ file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int
+) -> list[ImageFile._Tile]:
+ tiles = []
+ read = file.read
+
+ compression = i16(read(2))
+
+ xsize = bbox[2] - bbox[0]
+ ysize = bbox[3] - bbox[1]
+
+ offset = file.tell()
+
+ if compression == 0:
+ #
+ # raw compression
+ for channel in range(channels):
+ layer = mode[channel]
+ if mode == "CMYK":
+ layer += ";I"
+ tiles.append(ImageFile._Tile("raw", bbox, offset, layer))
+ offset = offset + xsize * ysize
+
+ elif compression == 1:
+ #
+ # packbits compression
+ i = 0
+ bytecount = read(channels * ysize * 2)
+ offset = file.tell()
+ for channel in range(channels):
+ layer = mode[channel]
+ if mode == "CMYK":
+ layer += ";I"
+ tiles.append(ImageFile._Tile("packbits", bbox, offset, layer))
+ for y in range(ysize):
+ offset = offset + i16(bytecount, i)
+ i += 2
+
+ file.seek(offset)
+
+ if offset & 1:
+ read(1) # padding
+
+ return tiles
+
+
+# --------------------------------------------------------------------
+# registry
+
+
+Image.register_open(PsdImageFile.format, PsdImageFile, _accept)
+
+Image.register_extension(PsdImageFile.format, ".psd")
+
+Image.register_mime(PsdImageFile.format, "image/vnd.adobe.photoshop")
diff --git a/venv/lib/python3.11/site-packages/PIL/QoiImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/QoiImagePlugin.py
new file mode 100644
index 000000000..e7a6cee16
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/QoiImagePlugin.py
@@ -0,0 +1,235 @@
+#
+# The Python Imaging Library.
+#
+# QOI support for PIL
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import os
+from typing import IO
+
+from . import Image, ImageFile
+from ._binary import i32be as i32
+from ._binary import o8
+from ._binary import o32be as o32
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(b"qoif")
+
+
+class QoiImageFile(ImageFile.ImageFile):
+ format = "QOI"
+ format_description = "Quite OK Image"
+
+ def _open(self) -> None:
+ assert self.fp is not None
+ if not _accept(self.fp.read(4)):
+ msg = "not a QOI file"
+ raise SyntaxError(msg)
+
+ self._size = i32(self.fp.read(4)), i32(self.fp.read(4))
+
+ channels = self.fp.read(1)[0]
+ self._mode = "RGB" if channels == 3 else "RGBA"
+
+ self.fp.seek(1, os.SEEK_CUR) # colorspace
+ self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell())]
+
+
+class QoiDecoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+ _previous_pixel: bytes | bytearray | None = None
+ _previously_seen_pixels: dict[int, bytes | bytearray] = {}
+
+ def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
+ self._previous_pixel = value
+
+ r, g, b, a = value
+ hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
+ self._previously_seen_pixels[hash_value] = value
+
+ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
+ assert self.fd is not None
+
+ self._previously_seen_pixels = {}
+ self._previous_pixel = bytearray((0, 0, 0, 255))
+
+ data = bytearray()
+ bands = Image.getmodebands(self.mode)
+ dest_length = self.state.xsize * self.state.ysize * bands
+ while len(data) < dest_length:
+ byte = self.fd.read(1)[0]
+ value: bytes | bytearray
+ if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
+ value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
+ elif byte == 0b11111111: # QOI_OP_RGBA
+ value = self.fd.read(4)
+ else:
+ op = byte >> 6
+ if op == 0: # QOI_OP_INDEX
+ op_index = byte & 0b00111111
+ value = self._previously_seen_pixels.get(
+ op_index, bytearray((0, 0, 0, 0))
+ )
+ elif op == 1 and self._previous_pixel: # QOI_OP_DIFF
+ value = bytearray(
+ (
+ (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
+ % 256,
+ (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2)
+ % 256,
+ (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256,
+ self._previous_pixel[3],
+ )
+ )
+ elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
+ second_byte = self.fd.read(1)[0]
+ diff_green = (byte & 0b00111111) - 32
+ diff_red = ((second_byte & 0b11110000) >> 4) - 8
+ diff_blue = (second_byte & 0b00001111) - 8
+
+ value = bytearray(
+ tuple(
+ (self._previous_pixel[i] + diff_green + diff) % 256
+ for i, diff in enumerate((diff_red, 0, diff_blue))
+ )
+ )
+ value += self._previous_pixel[3:]
+ elif op == 3 and self._previous_pixel: # QOI_OP_RUN
+ run_length = (byte & 0b00111111) + 1
+ value = self._previous_pixel
+ if bands == 3:
+ value = value[:3]
+ data += value * run_length
+ continue
+ self._add_to_previous_pixels(value)
+
+ if bands == 3:
+ value = value[:3]
+ data += value
+ self.set_as_raw(data)
+ return -1, 0
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode == "RGB":
+ channels = 3
+ elif im.mode == "RGBA":
+ channels = 4
+ else:
+ msg = "Unsupported QOI image mode"
+ raise ValueError(msg)
+
+ colorspace = 0 if im.encoderinfo.get("colorspace") == "sRGB" else 1
+
+ fp.write(b"qoif")
+ fp.write(o32(im.size[0]))
+ fp.write(o32(im.size[1]))
+ fp.write(o8(channels))
+ fp.write(o8(colorspace))
+
+ ImageFile._save(im, fp, [ImageFile._Tile("qoi", (0, 0) + im.size)])
+
+
+class QoiEncoder(ImageFile.PyEncoder):
+ _pushes_fd = True
+ _previous_pixel: tuple[int, int, int, int] | None = None
+ _previously_seen_pixels: dict[int, tuple[int, int, int, int]] = {}
+ _run = 0
+
+ def _write_run(self) -> bytes:
+ data = o8(0b11000000 | (self._run - 1)) # QOI_OP_RUN
+ self._run = 0
+ return data
+
+ def _delta(self, left: int, right: int) -> int:
+ result = (left - right) & 255
+ if result >= 128:
+ result -= 256
+ return result
+
+ def encode(self, bufsize: int) -> tuple[int, int, bytes]:
+ assert self.im is not None
+
+ self._previously_seen_pixels = {0: (0, 0, 0, 0)}
+ self._previous_pixel = (0, 0, 0, 255)
+
+ data = bytearray()
+ w, h = self.im.size
+ bands = Image.getmodebands(self.mode)
+
+ for y in range(h):
+ for x in range(w):
+ pixel = self.im.getpixel((x, y))
+ if bands == 3:
+ pixel = (*pixel, 255)
+
+ if pixel == self._previous_pixel:
+ self._run += 1
+ if self._run == 62:
+ data += self._write_run()
+ else:
+ if self._run:
+ data += self._write_run()
+
+ r, g, b, a = pixel
+ hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
+ if self._previously_seen_pixels.get(hash_value) == pixel:
+ data += o8(hash_value) # QOI_OP_INDEX
+ elif self._previous_pixel:
+ self._previously_seen_pixels[hash_value] = pixel
+
+ prev_r, prev_g, prev_b, prev_a = self._previous_pixel
+ if prev_a == a:
+ delta_r = self._delta(r, prev_r)
+ delta_g = self._delta(g, prev_g)
+ delta_b = self._delta(b, prev_b)
+
+ if (
+ -2 <= delta_r < 2
+ and -2 <= delta_g < 2
+ and -2 <= delta_b < 2
+ ):
+ data += o8(
+ 0b01000000
+ | (delta_r + 2) << 4
+ | (delta_g + 2) << 2
+ | (delta_b + 2)
+ ) # QOI_OP_DIFF
+ else:
+ delta_gr = self._delta(delta_r, delta_g)
+ delta_gb = self._delta(delta_b, delta_g)
+ if (
+ -8 <= delta_gr < 8
+ and -32 <= delta_g < 32
+ and -8 <= delta_gb < 8
+ ):
+ data += o8(
+ 0b10000000 | (delta_g + 32)
+ ) # QOI_OP_LUMA
+ data += o8((delta_gr + 8) << 4 | (delta_gb + 8))
+ else:
+ data += o8(0b11111110) # QOI_OP_RGB
+ data += bytes(pixel[:3])
+ else:
+ data += o8(0b11111111) # QOI_OP_RGBA
+ data += bytes(pixel)
+
+ self._previous_pixel = pixel
+
+ if self._run:
+ data += self._write_run()
+ data += bytes((0, 0, 0, 0, 0, 0, 0, 1)) # padding
+
+ return len(data), 0, bytes(data)
+
+
+Image.register_open(QoiImageFile.format, QoiImageFile, _accept)
+Image.register_decoder("qoi", QoiDecoder)
+Image.register_extension(QoiImageFile.format, ".qoi")
+
+Image.register_save(QoiImageFile.format, _save)
+Image.register_encoder("qoi", QoiEncoder)
diff --git a/venv/lib/python3.11/site-packages/PIL/SgiImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/SgiImagePlugin.py
new file mode 100644
index 000000000..76688ba57
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/SgiImagePlugin.py
@@ -0,0 +1,231 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# SGI image file handling
+#
+# See "The SGI Image File Format (Draft version 0.97)", Paul Haeberli.
+#
+#
+#
+# History:
+# 2017-22-07 mb Add RLE decompression
+# 2016-16-10 mb Add save method without compression
+# 1995-09-10 fl Created
+#
+# Copyright (c) 2016 by Mickael Bonfill.
+# Copyright (c) 2008 by Karsten Hiddemann.
+# Copyright (c) 1997 by Secret Labs AB.
+# Copyright (c) 1995 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import os
+import struct
+from typing import IO
+
+from . import Image, ImageFile
+from ._binary import i16be as i16
+from ._binary import o8
+
+
+def _accept(prefix: bytes) -> bool:
+ return len(prefix) >= 2 and i16(prefix) == 474
+
+
+MODES = {
+ (1, 1, 1): "L",
+ (1, 2, 1): "L",
+ (2, 1, 1): "L;16B",
+ (2, 2, 1): "L;16B",
+ (1, 3, 3): "RGB",
+ (2, 3, 3): "RGB;16B",
+ (1, 3, 4): "RGBA",
+ (2, 3, 4): "RGBA;16B",
+}
+
+
+##
+# Image plugin for SGI images.
+class SgiImageFile(ImageFile.ImageFile):
+ format = "SGI"
+ format_description = "SGI Image File Format"
+
+ def _open(self) -> None:
+ # HEAD
+ assert self.fp is not None
+
+ headlen = 512
+ s = self.fp.read(headlen)
+
+ if not _accept(s):
+ msg = "Not an SGI image file"
+ raise ValueError(msg)
+
+ # compression : verbatim or RLE
+ compression = s[2]
+
+ # bpc : 1 or 2 bytes (8bits or 16bits)
+ bpc = s[3]
+
+ # dimension : 1, 2 or 3 (depending on xsize, ysize and zsize)
+ dimension = i16(s, 4)
+
+ # xsize : width
+ xsize = i16(s, 6)
+
+ # ysize : height
+ ysize = i16(s, 8)
+
+ # zsize : channels count
+ zsize = i16(s, 10)
+
+ # determine mode from bits/zsize
+ try:
+ rawmode = MODES[(bpc, dimension, zsize)]
+ except KeyError:
+ msg = "Unsupported SGI image mode"
+ raise ValueError(msg)
+
+ self._size = xsize, ysize
+ self._mode = rawmode.split(";")[0]
+ if self.mode == "RGB":
+ self.custom_mimetype = "image/rgb"
+
+ # orientation -1 : scanlines begins at the bottom-left corner
+ orientation = -1
+
+ # decoder info
+ if compression == 0:
+ pagesize = xsize * ysize * bpc
+ if bpc == 2:
+ self.tile = [
+ ImageFile._Tile(
+ "SGI16",
+ (0, 0) + self.size,
+ headlen,
+ (self.mode, 0, orientation),
+ )
+ ]
+ else:
+ self.tile = []
+ offset = headlen
+ for layer in self.mode:
+ self.tile.append(
+ ImageFile._Tile(
+ "raw", (0, 0) + self.size, offset, (layer, 0, orientation)
+ )
+ )
+ offset += pagesize
+ elif compression == 1:
+ self.tile = [
+ ImageFile._Tile(
+ "sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)
+ )
+ ]
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode not in {"RGB", "RGBA", "L"}:
+ msg = "Unsupported SGI image mode"
+ raise ValueError(msg)
+
+ # Get the keyword arguments
+ info = im.encoderinfo
+
+ # Byte-per-pixel precision, 1 = 8bits per pixel
+ bpc = info.get("bpc", 1)
+
+ if bpc not in (1, 2):
+ msg = "Unsupported number of bytes per pixel"
+ raise ValueError(msg)
+
+ # Flip the image, since the origin of SGI file is the bottom-left corner
+ orientation = -1
+ # Define the file as SGI File Format
+ magic_number = 474
+ # Run-Length Encoding Compression - Unsupported at this time
+ rle = 0
+
+ # X Dimension = width / Y Dimension = height
+ x, y = im.size
+ # Z Dimension: Number of channels
+ z = len(im.mode)
+ # Number of dimensions (x,y,z)
+ if im.mode == "L":
+ dimension = 1 if y == 1 else 2
+ else:
+ dimension = 3
+
+ # Minimum Byte value
+ pinmin = 0
+ # Maximum Byte value (255 = 8bits per pixel)
+ pinmax = 255
+ # Image name (79 characters max, truncated below in write)
+ img_name = os.path.splitext(os.path.basename(filename))[0]
+ if isinstance(img_name, str):
+ img_name = img_name.encode("ascii", "ignore")
+ # Standard representation of pixel in the file
+ colormap = 0
+ fp.write(struct.pack(">h", magic_number))
+ fp.write(o8(rle))
+ fp.write(o8(bpc))
+ fp.write(struct.pack(">H", dimension))
+ fp.write(struct.pack(">H", x))
+ fp.write(struct.pack(">H", y))
+ fp.write(struct.pack(">H", z))
+ fp.write(struct.pack(">l", pinmin))
+ fp.write(struct.pack(">l", pinmax))
+ fp.write(struct.pack("4s", b"")) # dummy
+ fp.write(struct.pack("79s", img_name)) # truncates to 79 chars
+ fp.write(struct.pack("s", b"")) # force null byte after img_name
+ fp.write(struct.pack(">l", colormap))
+ fp.write(struct.pack("404s", b"")) # dummy
+
+ rawmode = "L"
+ if bpc == 2:
+ rawmode = "L;16B"
+
+ for channel in im.split():
+ fp.write(channel.tobytes("raw", rawmode, 0, orientation))
+
+ if hasattr(fp, "flush"):
+ fp.flush()
+
+
+class SGI16Decoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+
+ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
+ assert self.fd is not None
+ assert self.im is not None
+
+ rawmode, stride, orientation = self.args
+ pagesize = self.state.xsize * self.state.ysize
+ zsize = len(self.mode)
+ self.fd.seek(512)
+
+ for band in range(zsize):
+ channel = Image.new("L", (self.state.xsize, self.state.ysize))
+ channel.frombytes(
+ self.fd.read(2 * pagesize), "raw", "L;16B", stride, orientation
+ )
+ self.im.putband(channel.im, band)
+
+ return -1, 0
+
+
+#
+# registry
+
+
+Image.register_decoder("SGI16", SGI16Decoder)
+Image.register_open(SgiImageFile.format, SgiImageFile, _accept)
+Image.register_save(SgiImageFile.format, _save)
+Image.register_mime(SgiImageFile.format, "image/sgi")
+
+Image.register_extensions(SgiImageFile.format, [".bw", ".rgb", ".rgba", ".sgi"])
+
+# End of file
diff --git a/venv/lib/python3.11/site-packages/PIL/SpiderImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/SpiderImagePlugin.py
new file mode 100644
index 000000000..b8bf2282d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/SpiderImagePlugin.py
@@ -0,0 +1,332 @@
+#
+# The Python Imaging Library.
+#
+# SPIDER image file handling
+#
+# History:
+# 2004-08-02 Created BB
+# 2006-03-02 added save method
+# 2006-03-13 added support for stack images
+#
+# Copyright (c) 2004 by Health Research Inc. (HRI) RENSSELAER, NY 12144.
+# Copyright (c) 2004 by William Baxter.
+# Copyright (c) 2004 by Secret Labs AB.
+# Copyright (c) 2004 by Fredrik Lundh.
+#
+
+##
+# Image plugin for the Spider image format. This format is used
+# by the SPIDER software, in processing image data from electron
+# microscopy and tomography.
+##
+
+#
+# SpiderImagePlugin.py
+#
+# The Spider image format is used by SPIDER software, in processing
+# image data from electron microscopy and tomography.
+#
+# Spider home page:
+# https://spider.wadsworth.org/spider_doc/spider/docs/spider.html
+#
+# Details about the Spider image format:
+# https://spider.wadsworth.org/spider_doc/spider/docs/image_doc.html
+#
+from __future__ import annotations
+
+import os
+import struct
+import sys
+from typing import IO, Any
+
+from . import Image, ImageFile
+from ._util import DeferredError
+
+TYPE_CHECKING = False
+
+
+def isInt(f: Any) -> int:
+ try:
+ i = int(f)
+ if f - i == 0:
+ return 1
+ else:
+ return 0
+ except (ValueError, OverflowError):
+ return 0
+
+
+iforms = [1, 3, -11, -12, -21, -22]
+
+
+# There is no magic number to identify Spider files, so just check a
+# series of header locations to see if they have reasonable values.
+# Returns no. of bytes in the header, if it is a valid Spider header,
+# otherwise returns 0
+
+
+def isSpiderHeader(t: tuple[float, ...]) -> int:
+ h = (99,) + t # add 1 value so can use spider header index start=1
+ # header values 1,2,5,12,13,22,23 should be integers
+ for i in [1, 2, 5, 12, 13, 22, 23]:
+ if not isInt(h[i]):
+ return 0
+ # check iform
+ iform = int(h[5])
+ if iform not in iforms:
+ return 0
+ # check other header values
+ labrec = int(h[13]) # no. records in file header
+ labbyt = int(h[22]) # total no. of bytes in header
+ lenbyt = int(h[23]) # record length in bytes
+ if labbyt != (labrec * lenbyt):
+ return 0
+ # looks like a valid header
+ return labbyt
+
+
+def isSpiderImage(filename: str) -> int:
+ with open(filename, "rb") as fp:
+ f = fp.read(92) # read 23 * 4 bytes
+ t = struct.unpack(">23f", f) # try big-endian first
+ hdrlen = isSpiderHeader(t)
+ if hdrlen == 0:
+ t = struct.unpack("<23f", f) # little-endian
+ hdrlen = isSpiderHeader(t)
+ return hdrlen
+
+
+class SpiderImageFile(ImageFile.ImageFile):
+ format = "SPIDER"
+ format_description = "Spider 2D image"
+ _close_exclusive_fp_after_loading = False
+
+ def _open(self) -> None:
+ # check header
+ n = 27 * 4 # read 27 float values
+ assert self.fp is not None
+ f = self.fp.read(n)
+
+ try:
+ self.bigendian = 1
+ t = struct.unpack(">27f", f) # try big-endian first
+ hdrlen = isSpiderHeader(t)
+ if hdrlen == 0:
+ self.bigendian = 0
+ t = struct.unpack("<27f", f) # little-endian
+ hdrlen = isSpiderHeader(t)
+ if hdrlen == 0:
+ msg = "not a valid Spider file"
+ raise SyntaxError(msg)
+ except struct.error as e:
+ msg = "not a valid Spider file"
+ raise SyntaxError(msg) from e
+
+ h = (99,) + t # add 1 value : spider header index starts at 1
+ iform = int(h[5])
+ if iform != 1:
+ msg = "not a Spider 2D image"
+ raise SyntaxError(msg)
+
+ self._size = int(h[12]), int(h[2]) # size in pixels (width, height)
+ self.istack = int(h[24])
+ self.imgnumber = int(h[27])
+
+ if self.istack == 0 and self.imgnumber == 0:
+ # stk=0, img=0: a regular 2D image
+ offset = hdrlen
+ self._nimages = 1
+ elif self.istack > 0 and self.imgnumber == 0:
+ # stk>0, img=0: Opening the stack for the first time
+ self.imgbytes = int(h[12]) * int(h[2]) * 4
+ self.hdrlen = hdrlen
+ self._nimages = int(h[26])
+ # Point to the first image in the stack
+ offset = hdrlen * 2
+ self.imgnumber = 1
+ elif self.istack == 0 and self.imgnumber > 0:
+ # stk=0, img>0: an image within the stack
+ offset = hdrlen + self.stkoffset
+ self.istack = 2 # So Image knows it's still a stack
+ else:
+ msg = "inconsistent stack header values"
+ raise SyntaxError(msg)
+
+ if self.bigendian:
+ self.rawmode = "F;32BF"
+ else:
+ self.rawmode = "F;32F"
+ self._mode = "F"
+
+ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, offset, self.rawmode)]
+ self._fp = self.fp # FIXME: hack
+
+ @property
+ def n_frames(self) -> int:
+ return self._nimages
+
+ @property
+ def is_animated(self) -> bool:
+ return self._nimages > 1
+
+ # 1st image index is zero (although SPIDER imgnumber starts at 1)
+ def tell(self) -> int:
+ if self.imgnumber < 1:
+ return 0
+ else:
+ return self.imgnumber - 1
+
+ def seek(self, frame: int) -> None:
+ if self.istack == 0:
+ msg = "attempt to seek in a non-stack file"
+ raise EOFError(msg)
+ if not self._seek_check(frame):
+ return
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
+ self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes)
+ self.fp = self._fp
+ self.fp.seek(self.stkoffset)
+ self._open()
+
+ # returns a byte image after rescaling to 0..255
+ def convert2byte(self, depth: int = 255) -> Image.Image:
+ extrema = self.getextrema()
+ assert isinstance(extrema[0], float)
+ minimum, maximum = extrema
+ m: float = 1
+ if maximum != minimum:
+ m = depth / (maximum - minimum)
+ b = -m * minimum
+ return self.point(lambda i: i * m + b).convert("L")
+
+ if TYPE_CHECKING:
+ from . import ImageTk
+
+ # returns a ImageTk.PhotoImage object, after rescaling to 0..255
+ def tkPhotoImage(self) -> ImageTk.PhotoImage:
+ from . import ImageTk
+
+ return ImageTk.PhotoImage(self.convert2byte(), palette=256)
+
+
+# --------------------------------------------------------------------
+# Image series
+
+
+# given a list of filenames, return a list of images
+def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None:
+ """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
+ if filelist is None or len(filelist) < 1:
+ return None
+
+ byte_imgs = []
+ for img in filelist:
+ if not os.path.exists(img):
+ print(f"unable to find {img}")
+ continue
+ try:
+ with Image.open(img) as im:
+ assert isinstance(im, SpiderImageFile)
+ byte_im = im.convert2byte()
+ except Exception:
+ if not isSpiderImage(img):
+ print(f"{img} is not a Spider image file")
+ continue
+ byte_im.info["filename"] = img
+ byte_imgs.append(byte_im)
+ return byte_imgs
+
+
+# --------------------------------------------------------------------
+# For saving images in Spider format
+
+
+def makeSpiderHeader(im: Image.Image) -> list[bytes]:
+ nsam, nrow = im.size
+ lenbyt = max(1, nsam) * 4 # There are labrec records in the header
+ labrec = int(1024 / lenbyt)
+ if 1024 % lenbyt != 0:
+ labrec += 1
+ labbyt = labrec * lenbyt
+ nvalues = int(labbyt / 4)
+ if nvalues < 23:
+ return []
+
+ hdr = [0.0] * nvalues
+
+ # NB these are Fortran indices
+ hdr[1] = 1.0 # nslice (=1 for an image)
+ hdr[2] = float(nrow) # number of rows per slice
+ hdr[3] = float(nrow) # number of records in the image
+ hdr[5] = 1.0 # iform for 2D image
+ hdr[12] = float(nsam) # number of pixels per line
+ hdr[13] = float(labrec) # number of records in file header
+ hdr[22] = float(labbyt) # total number of bytes in header
+ hdr[23] = float(lenbyt) # record length in bytes
+
+ # adjust for Fortran indexing
+ hdr = hdr[1:]
+ hdr.append(0.0)
+ # pack binary data into a string
+ return [struct.pack("f", v) for v in hdr]
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode != "F":
+ im = im.convert("F")
+
+ hdr = makeSpiderHeader(im)
+ if len(hdr) < 256:
+ msg = "Error creating Spider header"
+ raise OSError(msg)
+
+ # write the SPIDER header
+ fp.writelines(hdr)
+
+ rawmode = "F;32NF" # 32-bit native floating point
+ ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, rawmode)])
+
+
+def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ # get the filename extension and register it with Image
+ if filename_ext := os.path.splitext(filename)[1]:
+ ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
+ Image.register_extension(SpiderImageFile.format, ext)
+ _save(im, fp, filename)
+
+
+# --------------------------------------------------------------------
+
+
+Image.register_open(SpiderImageFile.format, SpiderImageFile)
+Image.register_save(SpiderImageFile.format, _save_spider)
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]")
+ sys.exit()
+
+ filename = sys.argv[1]
+ if not isSpiderImage(filename):
+ print("input image must be in Spider format")
+ sys.exit()
+
+ with Image.open(filename) as im:
+ print(f"image: {im}")
+ print(f"format: {im.format}")
+ print(f"size: {im.size}")
+ print(f"mode: {im.mode}")
+ print("max, min: ", end=" ")
+ print(im.getextrema())
+
+ if len(sys.argv) > 2:
+ outfile = sys.argv[2]
+
+ # perform some image operation
+ transposed_im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+ print(
+ f"saving a flipped version of {os.path.basename(filename)} "
+ f"as {outfile} "
+ )
+ transposed_im.save(outfile, SpiderImageFile.format)
diff --git a/venv/lib/python3.11/site-packages/PIL/SunImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/SunImagePlugin.py
new file mode 100644
index 000000000..8912379ea
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/SunImagePlugin.py
@@ -0,0 +1,145 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# Sun image file handling
+#
+# History:
+# 1995-09-10 fl Created
+# 1996-05-28 fl Fixed 32-bit alignment
+# 1998-12-29 fl Import ImagePalette module
+# 2001-12-18 fl Fixed palette loading (from Jean-Claude Rimbault)
+#
+# Copyright (c) 1997-2001 by Secret Labs AB
+# Copyright (c) 1995-1996 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import i32be as i32
+
+
+def _accept(prefix: bytes) -> bool:
+ return len(prefix) >= 4 and i32(prefix) == 0x59A66A95
+
+
+##
+# Image plugin for Sun raster files.
+
+
+class SunImageFile(ImageFile.ImageFile):
+ format = "SUN"
+ format_description = "Sun Raster File"
+
+ def _open(self) -> None:
+ # The Sun Raster file header is 32 bytes in length
+ # and has the following format:
+
+ # typedef struct _SunRaster
+ # {
+ # DWORD MagicNumber; /* Magic (identification) number */
+ # DWORD Width; /* Width of image in pixels */
+ # DWORD Height; /* Height of image in pixels */
+ # DWORD Depth; /* Number of bits per pixel */
+ # DWORD Length; /* Size of image data in bytes */
+ # DWORD Type; /* Type of raster file */
+ # DWORD ColorMapType; /* Type of color map */
+ # DWORD ColorMapLength; /* Size of the color map in bytes */
+ # } SUNRASTER;
+
+ assert self.fp is not None
+
+ # HEAD
+ s = self.fp.read(32)
+ if not _accept(s):
+ msg = "not an SUN raster file"
+ raise SyntaxError(msg)
+
+ offset = 32
+
+ self._size = i32(s, 4), i32(s, 8)
+
+ depth = i32(s, 12)
+ # data_length = i32(s, 16) # unreliable, ignore.
+ file_type = i32(s, 20)
+ palette_type = i32(s, 24) # 0: None, 1: RGB, 2: Raw/arbitrary
+ palette_length = i32(s, 28)
+
+ if depth == 1:
+ self._mode, rawmode = "1", "1;I"
+ elif depth == 4:
+ self._mode, rawmode = "L", "L;4"
+ elif depth == 8:
+ self._mode = rawmode = "L"
+ elif depth == 24:
+ if file_type == 3:
+ self._mode, rawmode = "RGB", "RGB"
+ else:
+ self._mode, rawmode = "RGB", "BGR"
+ elif depth == 32:
+ if file_type == 3:
+ self._mode, rawmode = "RGB", "RGBX"
+ else:
+ self._mode, rawmode = "RGB", "BGRX"
+ else:
+ msg = "Unsupported Mode/Bit Depth"
+ raise SyntaxError(msg)
+
+ if palette_length:
+ if palette_length > 1024:
+ msg = "Unsupported Color Palette Length"
+ raise SyntaxError(msg)
+
+ if palette_type != 1:
+ msg = "Unsupported Palette Type"
+ raise SyntaxError(msg)
+
+ offset = offset + palette_length
+ self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length))
+ if self.mode == "L":
+ self._mode = "P"
+ rawmode = rawmode.replace("L", "P")
+
+ # 16 bit boundaries on stride
+ stride = ((self.size[0] * depth + 15) // 16) * 2
+
+ # file type: Type is the version (or flavor) of the bitmap
+ # file. The following values are typically found in the Type
+ # field:
+ # 0000h Old
+ # 0001h Standard
+ # 0002h Byte-encoded
+ # 0003h RGB format
+ # 0004h TIFF format
+ # 0005h IFF format
+ # FFFFh Experimental
+
+ # Old and standard are the same, except for the length tag.
+ # byte-encoded is run-length-encoded
+ # RGB looks similar to standard, but RGB byte order
+ # TIFF and IFF mean that they were converted from T/IFF
+ # Experimental means that it's something else.
+ # (https://www.fileformat.info/format/sunraster/egff.htm)
+
+ if file_type in (0, 1, 3, 4, 5):
+ self.tile = [
+ ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride))
+ ]
+ elif file_type == 2:
+ self.tile = [
+ ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode)
+ ]
+ else:
+ msg = "Unsupported Sun Raster file type"
+ raise SyntaxError(msg)
+
+
+#
+# registry
+
+
+Image.register_open(SunImageFile.format, SunImageFile, _accept)
+
+Image.register_extension(SunImageFile.format, ".ras")
diff --git a/venv/lib/python3.11/site-packages/PIL/TarIO.py b/venv/lib/python3.11/site-packages/PIL/TarIO.py
new file mode 100644
index 000000000..86490a496
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/TarIO.py
@@ -0,0 +1,61 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# read files from within a tar file
+#
+# History:
+# 95-06-18 fl Created
+# 96-05-28 fl Open files in binary mode
+#
+# Copyright (c) Secret Labs AB 1997.
+# Copyright (c) Fredrik Lundh 1995-96.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+
+from . import ContainerIO
+
+
+class TarIO(ContainerIO.ContainerIO[bytes]):
+ """A file object that provides read access to a given member of a TAR file."""
+
+ def __init__(self, tarfile: str, file: str) -> None:
+ """
+ Create file object.
+
+ :param tarfile: Name of TAR file.
+ :param file: Name of member file.
+ """
+ self.fh = open(tarfile, "rb")
+
+ while True:
+ s = self.fh.read(512)
+ if len(s) != 512:
+ self.fh.close()
+
+ msg = "unexpected end of tar file"
+ raise OSError(msg)
+
+ name = s[:100].decode("utf-8")
+ i = name.find("\0")
+ if i == 0:
+ self.fh.close()
+
+ msg = "cannot find subfile"
+ raise OSError(msg)
+ if i > 0:
+ name = name[:i]
+
+ size = int(s[124:135], 8)
+
+ if file == name:
+ break
+
+ self.fh.seek((size + 511) & (~511), io.SEEK_CUR)
+
+ # Open region
+ super().__init__(self.fh, self.fh.tell(), size)
diff --git a/venv/lib/python3.11/site-packages/PIL/TgaImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/TgaImagePlugin.py
new file mode 100644
index 000000000..8fd63b1df
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/TgaImagePlugin.py
@@ -0,0 +1,284 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# TGA file handling
+#
+# History:
+# 95-09-01 fl created (reads 24-bit files only)
+# 97-01-04 fl support more TGA versions, including compressed images
+# 98-07-04 fl fixed orientation and alpha layer bugs
+# 98-09-11 fl fixed orientation for runlength decoder
+#
+# Copyright (c) Secret Labs AB 1997-98.
+# Copyright (c) Fredrik Lundh 1995-97.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import os
+import warnings
+from typing import IO
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import i16le as i16
+from ._binary import i32le as i32
+from ._binary import o8
+from ._binary import o16le as o16
+
+#
+# --------------------------------------------------------------------
+# Read RGA file
+
+
+MODES = {
+ # map imagetype/depth to rawmode
+ (1, 8): "P",
+ (3, 1): "1",
+ (3, 8): "L",
+ (3, 16): "LA",
+ (2, 16): "BGRA;15Z",
+ (2, 24): "BGR",
+ (2, 32): "BGRA",
+}
+
+
+##
+# Image plugin for Targa files.
+
+
+class TgaImageFile(ImageFile.ImageFile):
+ format = "TGA"
+ format_description = "Targa"
+
+ def _open(self) -> None:
+ # process header
+ assert self.fp is not None
+
+ s = self.fp.read(18)
+
+ id_len = s[0]
+
+ colormaptype = s[1]
+ imagetype = s[2]
+
+ depth = s[16]
+
+ flags = s[17]
+
+ self._size = i16(s, 12), i16(s, 14)
+
+ # validate header fields
+ if (
+ colormaptype not in (0, 1)
+ or self.size[0] <= 0
+ or self.size[1] <= 0
+ or depth not in (1, 8, 16, 24, 32)
+ ):
+ msg = "not a TGA file"
+ raise SyntaxError(msg)
+
+ # image mode
+ if imagetype in (3, 11):
+ self._mode = "L"
+ if depth == 1:
+ self._mode = "1" # ???
+ elif depth == 16:
+ self._mode = "LA"
+ elif imagetype in (1, 9):
+ self._mode = "P" if colormaptype else "L"
+ elif imagetype in (2, 10):
+ self._mode = "RGB" if depth == 24 else "RGBA"
+ else:
+ msg = "unknown TGA mode"
+ raise SyntaxError(msg)
+
+ # orientation
+ orientation = flags & 0x30
+ self._flip_horizontally = orientation in [0x10, 0x30]
+ if orientation in [0x20, 0x30]:
+ orientation = 1
+ elif orientation in [0, 0x10]:
+ orientation = -1
+ else:
+ msg = "unknown TGA orientation"
+ raise SyntaxError(msg)
+
+ self.info["orientation"] = orientation
+
+ if imagetype & 8:
+ self.info["compression"] = "tga_rle"
+
+ if id_len:
+ self.info["id_section"] = self.fp.read(id_len)
+
+ if colormaptype:
+ # read palette
+ start, size, mapdepth = i16(s, 3), i16(s, 5), s[7]
+ if mapdepth == 16:
+ self.palette = ImagePalette.raw(
+ "BGRA;15Z", bytes(2 * start) + self.fp.read(2 * size)
+ )
+ self.palette.mode = "RGBA"
+ elif mapdepth == 24:
+ self.palette = ImagePalette.raw(
+ "BGR", bytes(3 * start) + self.fp.read(3 * size)
+ )
+ elif mapdepth == 32:
+ self.palette = ImagePalette.raw(
+ "BGRA", bytes(4 * start) + self.fp.read(4 * size)
+ )
+ else:
+ msg = "unknown TGA map depth"
+ raise SyntaxError(msg)
+
+ # setup tile descriptor
+ try:
+ rawmode = MODES[(imagetype & 7, depth)]
+ if imagetype & 8:
+ # compressed
+ self.tile = [
+ ImageFile._Tile(
+ "tga_rle",
+ (0, 0) + self.size,
+ self.fp.tell(),
+ (rawmode, orientation, depth),
+ )
+ ]
+ else:
+ self.tile = [
+ ImageFile._Tile(
+ "raw",
+ (0, 0) + self.size,
+ self.fp.tell(),
+ (rawmode, 0, orientation),
+ )
+ ]
+ except KeyError:
+ pass # cannot decode
+
+ def load_end(self) -> None:
+ if self.mode == "RGBA":
+ assert self.fp is not None
+ self.fp.seek(-26, os.SEEK_END)
+ footer = self.fp.read(26)
+ if footer.endswith(b"TRUEVISION-XFILE.\x00"):
+ # version 2
+ extension_offset = i32(footer)
+ if extension_offset:
+ self.fp.seek(extension_offset + 494)
+ attributes_type = self.fp.read(1)
+ if attributes_type == b"\x00":
+ # No alpha
+ self.im.fillband(3, 255)
+
+ if self._flip_horizontally:
+ self.im = self.im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+
+
+#
+# --------------------------------------------------------------------
+# Write TGA file
+
+
+SAVE = {
+ "1": ("1", 1, 0, 3),
+ "L": ("L", 8, 0, 3),
+ "LA": ("LA", 16, 0, 3),
+ "P": ("P", 8, 1, 1),
+ "RGB": ("BGR", 24, 0, 2),
+ "RGBA": ("BGRA", 32, 0, 2),
+}
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ try:
+ rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
+ except KeyError as e:
+ msg = f"cannot write mode {im.mode} as TGA"
+ raise OSError(msg) from e
+
+ if "rle" in im.encoderinfo:
+ rle = im.encoderinfo["rle"]
+ else:
+ compression = im.encoderinfo.get("compression", im.info.get("compression"))
+ rle = compression == "tga_rle"
+ if rle:
+ if im.mode == "1":
+ msg = f"cannot write mode {im.mode} as TGA with run-length encoding"
+ raise OSError(msg)
+
+ imagetype += 8
+
+ id_section = im.encoderinfo.get("id_section", im.info.get("id_section", ""))
+ id_len = len(id_section)
+ if id_len > 255:
+ id_len = 255
+ id_section = id_section[:255]
+ warnings.warn("id_section has been trimmed to 255 characters")
+
+ if colormaptype:
+ palette = im.im.getpalette("RGB", "BGR")
+ colormaplength, colormapentry = len(palette) // 3, 24
+ else:
+ colormaplength, colormapentry = 0, 0
+
+ if im.mode in ("LA", "RGBA"):
+ flags = 8
+ else:
+ flags = 0
+
+ orientation = im.encoderinfo.get("orientation", im.info.get("orientation", -1))
+ if orientation > 0:
+ flags = flags | 0x20
+
+ fp.write(
+ o8(id_len)
+ + o8(colormaptype)
+ + o8(imagetype)
+ + o16(0) # colormapfirst
+ + o16(colormaplength)
+ + o8(colormapentry)
+ + o16(0)
+ + o16(0)
+ + o16(im.size[0])
+ + o16(im.size[1])
+ + o8(bits)
+ + o8(flags)
+ )
+
+ if id_section:
+ fp.write(id_section)
+
+ if colormaptype:
+ fp.write(palette)
+
+ if rle:
+ ImageFile._save(
+ im,
+ fp,
+ [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))],
+ )
+ else:
+ ImageFile._save(
+ im,
+ fp,
+ [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))],
+ )
+
+ # write targa version 2 footer
+ fp.write(b"\000" * 8 + b"TRUEVISION-XFILE." + b"\000")
+
+
+#
+# --------------------------------------------------------------------
+# Registry
+
+
+Image.register_open(TgaImageFile.format, TgaImageFile)
+Image.register_save(TgaImageFile.format, _save)
+
+Image.register_extensions(TgaImageFile.format, [".tga", ".icb", ".vda", ".vst"])
+
+Image.register_mime(TgaImageFile.format, "image/x-tga")
diff --git a/venv/lib/python3.11/site-packages/PIL/TiffImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/TiffImagePlugin.py
new file mode 100644
index 000000000..472dfcf58
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/TiffImagePlugin.py
@@ -0,0 +1,2354 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# TIFF file handling
+#
+# TIFF is a flexible, if somewhat aged, image file format originally
+# defined by Aldus. Although TIFF supports a wide variety of pixel
+# layouts and compression methods, the name doesn't really stand for
+# "thousands of incompatible file formats," it just feels that way.
+#
+# To read TIFF data from a stream, the stream must be seekable. For
+# progressive decoding, make sure to use TIFF files where the tag
+# directory is placed first in the file.
+#
+# History:
+# 1995-09-01 fl Created
+# 1996-05-04 fl Handle JPEGTABLES tag
+# 1996-05-18 fl Fixed COLORMAP support
+# 1997-01-05 fl Fixed PREDICTOR support
+# 1997-08-27 fl Added support for rational tags (from Perry Stoll)
+# 1998-01-10 fl Fixed seek/tell (from Jan Blom)
+# 1998-07-15 fl Use private names for internal variables
+# 1999-06-13 fl Rewritten for PIL 1.0 (1.0)
+# 2000-10-11 fl Additional fixes for Python 2.0 (1.1)
+# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2)
+# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3)
+# 2001-12-18 fl Added workaround for broken Matrox library
+# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart)
+# 2003-05-19 fl Check FILLORDER tag
+# 2003-09-26 fl Added RGBa support
+# 2004-02-24 fl Added DPI support; fixed rational write support
+# 2005-02-07 fl Added workaround for broken Corel Draw 10 files
+# 2006-01-09 fl Added support for float/double tags (from Russell Nelson)
+#
+# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved.
+# Copyright (c) 1995-1997 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import io
+import itertools
+import logging
+import math
+import os
+import struct
+import warnings
+from collections.abc import Callable, MutableMapping
+from fractions import Fraction
+from numbers import Number, Rational
+from typing import IO, Any, cast
+
+from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
+from ._binary import i16be as i16
+from ._binary import i32be as i32
+from ._binary import o8
+from ._util import DeferredError, is_path
+from .TiffTags import TYPES
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from typing import NoReturn
+
+ from ._typing import Buffer, IntegralLike, StrOrBytesPath
+
+logger = logging.getLogger(__name__)
+
+# Set these to true to force use of libtiff for reading or writing.
+READ_LIBTIFF = False
+WRITE_LIBTIFF = False
+STRIP_SIZE = 65536
+
+II = b"II" # little-endian (Intel style)
+MM = b"MM" # big-endian (Motorola style)
+
+#
+# --------------------------------------------------------------------
+# Read TIFF files
+
+# a few tag names, just to make the code below a bit more readable
+OSUBFILETYPE = 255
+IMAGEWIDTH = 256
+IMAGELENGTH = 257
+BITSPERSAMPLE = 258
+COMPRESSION = 259
+PHOTOMETRIC_INTERPRETATION = 262
+FILLORDER = 266
+IMAGEDESCRIPTION = 270
+STRIPOFFSETS = 273
+SAMPLESPERPIXEL = 277
+ROWSPERSTRIP = 278
+STRIPBYTECOUNTS = 279
+X_RESOLUTION = 282
+Y_RESOLUTION = 283
+PLANAR_CONFIGURATION = 284
+RESOLUTION_UNIT = 296
+TRANSFERFUNCTION = 301
+SOFTWARE = 305
+DATE_TIME = 306
+ARTIST = 315
+PREDICTOR = 317
+COLORMAP = 320
+TILEWIDTH = 322
+TILELENGTH = 323
+TILEOFFSETS = 324
+TILEBYTECOUNTS = 325
+SUBIFD = 330
+EXTRASAMPLES = 338
+SAMPLEFORMAT = 339
+JPEGTABLES = 347
+YCBCRSUBSAMPLING = 530
+REFERENCEBLACKWHITE = 532
+COPYRIGHT = 33432
+IPTC_NAA_CHUNK = 33723 # newsphoto properties
+PHOTOSHOP_CHUNK = 34377 # photoshop properties
+ICCPROFILE = 34675
+EXIFIFD = 34665
+XMP = 700
+JPEGQUALITY = 65537 # pseudo-tag by libtiff
+
+# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
+IMAGEJ_META_DATA_BYTE_COUNTS = 50838
+IMAGEJ_META_DATA = 50839
+
+COMPRESSION_INFO = {
+ # Compression => pil compression name
+ 1: "raw",
+ 2: "tiff_ccitt",
+ 3: "group3",
+ 4: "group4",
+ 5: "tiff_lzw",
+ 6: "tiff_jpeg", # obsolete
+ 7: "jpeg",
+ 8: "tiff_adobe_deflate",
+ 32771: "tiff_raw_16", # 16-bit padding
+ 32773: "packbits",
+ 32809: "tiff_thunderscan",
+ 32946: "tiff_deflate",
+ 34676: "tiff_sgilog",
+ 34677: "tiff_sgilog24",
+ 34925: "lzma",
+ 50000: "zstd",
+ 50001: "webp",
+}
+
+COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}
+
+OPEN_INFO = {
+ # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
+ # ExtraSamples) => mode, rawmode
+ (II, 0, (1,), 1, (1,), ()): ("1", "1;I"),
+ (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"),
+ (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
+ (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
+ (II, 1, (1,), 1, (1,), ()): ("1", "1"),
+ (MM, 1, (1,), 1, (1,), ()): ("1", "1"),
+ (II, 1, (1,), 2, (1,), ()): ("1", "1;R"),
+ (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"),
+ (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
+ (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
+ (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
+ (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
+ (II, 1, (1,), 1, (2,), ()): ("L", "L;2"),
+ (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"),
+ (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
+ (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
+ (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
+ (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
+ (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
+ (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
+ (II, 1, (1,), 1, (4,), ()): ("L", "L;4"),
+ (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"),
+ (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
+ (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
+ (II, 0, (1,), 1, (8,), ()): ("L", "L;I"),
+ (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"),
+ (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
+ (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
+ (II, 1, (1,), 1, (8,), ()): ("L", "L"),
+ (MM, 1, (1,), 1, (8,), ()): ("L", "L"),
+ (II, 1, (2,), 1, (8,), ()): ("L", "L"),
+ (MM, 1, (2,), 1, (8,), ()): ("L", "L"),
+ (II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
+ (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
+ (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
+ (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
+ (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
+ (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
+ (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
+ (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
+ (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"),
+ (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"),
+ (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"),
+ (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"),
+ (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"),
+ (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"),
+ (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"),
+ (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"),
+ (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
+ (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
+ (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
+ (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
+ (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
+ (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
+ (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
+ (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
+ (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
+ (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
+ (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
+ (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
+ (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
+ (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
+ (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
+ (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
+ (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"),
+ (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"),
+ (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
+ (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
+ (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
+ (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"),
+ (II, 3, (1,), 1, (1,), ()): ("P", "P;1"),
+ (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"),
+ (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
+ (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
+ (II, 3, (1,), 1, (2,), ()): ("P", "P;2"),
+ (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"),
+ (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
+ (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
+ (II, 3, (1,), 1, (4,), ()): ("P", "P;4"),
+ (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"),
+ (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
+ (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
+ (II, 3, (1,), 1, (8,), ()): ("P", "P"),
+ (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
+ (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
+ (MM, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
+ (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
+ (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
+ (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
+ (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"),
+ (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
+ (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
+ (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
+ (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
+ (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
+ (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
+ (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
+ (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"),
+ (II, 6, (1,), 1, (8,), ()): ("L", "L"),
+ (MM, 6, (1,), 1, (8,), ()): ("L", "L"),
+ # JPEG compressed images handled by LibTiff and auto-converted to RGBX
+ # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
+ (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
+ (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
+ (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
+ (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
+}
+
+MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
+
+PREFIXES = [
+ b"MM\x00\x2a", # Valid TIFF header with big-endian byte order
+ b"II\x2a\x00", # Valid TIFF header with little-endian byte order
+ b"MM\x2a\x00", # Invalid TIFF header, assume big-endian
+ b"II\x00\x2a", # Invalid TIFF header, assume little-endian
+ b"MM\x00\x2b", # BigTIFF with big-endian byte order
+ b"II\x2b\x00", # BigTIFF with little-endian byte order
+]
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(tuple(PREFIXES))
+
+
+def _limit_rational(
+ val: float | Fraction | IFDRational, max_val: int
+) -> tuple[IntegralLike, IntegralLike]:
+ inv = abs(val) > 1
+ n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
+ return n_d[::-1] if inv else n_d
+
+
+def _limit_signed_rational(
+ val: IFDRational, max_val: int, min_val: int
+) -> tuple[IntegralLike, IntegralLike]:
+ frac = Fraction(val)
+ n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator
+
+ if min(float(i) for i in n_d) < min_val:
+ n_d = _limit_rational(val, abs(min_val))
+
+ n_d_float = tuple(float(i) for i in n_d)
+ if max(n_d_float) > max_val:
+ n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val)
+
+ return n_d
+
+
+##
+# Wrapper for TIFF IFDs.
+
+_load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {}
+_write_dispatch: dict[int, Callable[..., Any]] = {}
+
+
+def _delegate(op: str) -> Any:
+ def delegate(
+ self: IFDRational, *args: tuple[float, ...]
+ ) -> bool | float | Fraction:
+ return getattr(self._val, op)(*args)
+
+ return delegate
+
+
+class IFDRational(Rational):
+ """Implements a rational class where 0/0 is a legal value to match
+ the in the wild use of exif rationals.
+
+ e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used
+ """
+
+ """ If the denominator is 0, store this as a float('nan'), otherwise store
+ as a fractions.Fraction(). Delegate as appropriate
+
+ """
+
+ __slots__ = ("_numerator", "_denominator", "_val")
+
+ def __init__(
+ self, value: float | Fraction | IFDRational, denominator: int = 1
+ ) -> None:
+ """
+ :param value: either an integer numerator, a
+ float/rational/other number, or an IFDRational
+ :param denominator: Optional integer denominator
+ """
+ self._val: Fraction | float
+ if isinstance(value, IFDRational):
+ self._numerator = value.numerator
+ self._denominator = value.denominator
+ self._val = value._val
+ return
+
+ if isinstance(value, Fraction):
+ self._numerator = value.numerator
+ self._denominator = value.denominator
+ else:
+ if TYPE_CHECKING:
+ self._numerator = cast(IntegralLike, value)
+ else:
+ self._numerator = value
+ self._denominator = denominator
+
+ if denominator == 0:
+ self._val = float("nan")
+ elif denominator == 1:
+ self._val = Fraction(value)
+ elif int(value) == value:
+ self._val = Fraction(int(value), denominator)
+ else:
+ self._val = Fraction(value / denominator)
+
+ @property
+ def numerator(self) -> IntegralLike:
+ return self._numerator
+
+ @property
+ def denominator(self) -> int:
+ return self._denominator
+
+ def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]:
+ """
+
+ :param max_denominator: Integer, the maximum denominator value
+ :returns: Tuple of (numerator, denominator)
+ """
+
+ if self.denominator == 0:
+ return self.numerator, self.denominator
+
+ assert isinstance(self._val, Fraction)
+ f = self._val.limit_denominator(max_denominator)
+ return f.numerator, f.denominator
+
+ def __repr__(self) -> str:
+ return str(float(self._val))
+
+ def __hash__(self) -> int: # type: ignore[override]
+ return self._val.__hash__()
+
+ def __eq__(self, other: object) -> bool:
+ val = self._val
+ if isinstance(other, IFDRational):
+ other = other._val
+ if isinstance(other, float):
+ val = float(val)
+ return val == other
+
+ def __getstate__(self) -> list[float | Fraction | IntegralLike]:
+ return [self._val, self._numerator, self._denominator]
+
+ def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None:
+ IFDRational.__init__(self, 0)
+ _val, _numerator, _denominator = state
+ assert isinstance(_val, (float, Fraction))
+ self._val = _val
+ if TYPE_CHECKING:
+ self._numerator = cast(IntegralLike, _numerator)
+ else:
+ self._numerator = _numerator
+ assert isinstance(_denominator, int)
+ self._denominator = _denominator
+
+ """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
+ 'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
+ 'mod','rmod', 'pow','rpow', 'pos', 'neg',
+ 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',
+ 'ceil', 'floor', 'round']
+ print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a))
+ """
+
+ __add__ = _delegate("__add__")
+ __radd__ = _delegate("__radd__")
+ __sub__ = _delegate("__sub__")
+ __rsub__ = _delegate("__rsub__")
+ __mul__ = _delegate("__mul__")
+ __rmul__ = _delegate("__rmul__")
+ __truediv__ = _delegate("__truediv__")
+ __rtruediv__ = _delegate("__rtruediv__")
+ __floordiv__ = _delegate("__floordiv__")
+ __rfloordiv__ = _delegate("__rfloordiv__")
+ __mod__ = _delegate("__mod__")
+ __rmod__ = _delegate("__rmod__")
+ __pow__ = _delegate("__pow__")
+ __rpow__ = _delegate("__rpow__")
+ __pos__ = _delegate("__pos__")
+ __neg__ = _delegate("__neg__")
+ __abs__ = _delegate("__abs__")
+ __trunc__ = _delegate("__trunc__")
+ __lt__ = _delegate("__lt__")
+ __gt__ = _delegate("__gt__")
+ __le__ = _delegate("__le__")
+ __ge__ = _delegate("__ge__")
+ __bool__ = _delegate("__bool__")
+ __ceil__ = _delegate("__ceil__")
+ __floor__ = _delegate("__floor__")
+ __round__ = _delegate("__round__")
+ __float__ = _delegate("__float__")
+ # Python >= 3.11
+ if hasattr(Fraction, "__int__"):
+ __int__ = _delegate("__int__")
+
+
+_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any]
+
+
+def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]:
+ def decorator(func: _LoaderFunc) -> _LoaderFunc:
+ from .TiffTags import TYPES
+
+ if func.__name__.startswith("load_"):
+ TYPES[idx] = func.__name__[5:].replace("_", " ")
+ _load_dispatch[idx] = size, func # noqa: F821
+ return func
+
+ return decorator
+
+
+def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
+ def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
+ _write_dispatch[idx] = func # noqa: F821
+ return func
+
+ return decorator
+
+
+def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None:
+ from .TiffTags import TYPES
+
+ idx, fmt, name = idx_fmt_name
+ TYPES[idx] = name
+ size = struct.calcsize(f"={fmt}")
+
+ def basic_handler(
+ self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True
+ ) -> tuple[Any, ...]:
+ return self._unpack(f"{len(data) // size}{fmt}", data)
+
+ _load_dispatch[idx] = size, basic_handler # noqa: F821
+ _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
+ b"".join(self._pack(fmt, value) for value in values)
+ )
+
+
+if TYPE_CHECKING:
+ _IFDv2Base = MutableMapping[int, Any]
+else:
+ _IFDv2Base = MutableMapping
+
+
+class ImageFileDirectory_v2(_IFDv2Base):
+ """This class represents a TIFF tag directory. To speed things up, we
+ don't decode tags unless they're asked for.
+
+ Exposes a dictionary interface of the tags in the directory::
+
+ ifd = ImageFileDirectory_v2()
+ ifd[key] = 'Some Data'
+ ifd.tagtype[key] = TiffTags.ASCII
+ print(ifd[key])
+ 'Some Data'
+
+ Individual values are returned as the strings or numbers, sequences are
+ returned as tuples of the values.
+
+ The tiff metadata type of each item is stored in a dictionary of
+ tag types in
+ :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types
+ are read from a tiff file, guessed from the type added, or added
+ manually.
+
+ Data Structures:
+
+ * ``self.tagtype = {}``
+
+ * Key: numerical TIFF tag number
+ * Value: integer corresponding to the data type from
+ :py:data:`.TiffTags.TYPES`
+
+ .. versionadded:: 3.0.0
+
+ 'Internal' data structures:
+
+ * ``self._tags_v2 = {}``
+
+ * Key: numerical TIFF tag number
+ * Value: decoded data, as tuple for multiple values
+
+ * ``self._tagdata = {}``
+
+ * Key: numerical TIFF tag number
+ * Value: undecoded byte string from file
+
+ * ``self._tags_v1 = {}``
+
+ * Key: numerical TIFF tag number
+ * Value: decoded data in the v1 format
+
+ Tags will be found in the private attributes ``self._tagdata``, and in
+ ``self._tags_v2`` once decoded.
+
+ ``self.legacy_api`` is a value for internal use, and shouldn't be changed
+ from outside code. In cooperation with
+ :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``
+ is true, then decoded tags will be populated into both ``_tags_v1`` and
+ ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF
+ save routine. Tags should be read from ``_tags_v1`` if
+ ``legacy_api == true``.
+
+ """
+
+ _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {}
+ _write_dispatch: dict[int, Callable[..., Any]] = {}
+
+ def __init__(
+ self,
+ ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00",
+ prefix: bytes | None = None,
+ group: int | None = None,
+ ) -> None:
+ """Initialize an ImageFileDirectory.
+
+ To construct an ImageFileDirectory from a real file, pass the 8-byte
+ magic header to the constructor. To only set the endianness, pass it
+ as the 'prefix' keyword argument.
+
+ :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets
+ endianness.
+ :param prefix: Override the endianness of the file.
+ """
+ if not _accept(ifh):
+ msg = f"not a TIFF file (header {repr(ifh)} not valid)"
+ raise SyntaxError(msg)
+ self._prefix = prefix if prefix is not None else ifh[:2]
+ if self._prefix == MM:
+ self._endian = ">"
+ elif self._prefix == II:
+ self._endian = "<"
+ else:
+ msg = "not a TIFF IFD"
+ raise SyntaxError(msg)
+ self._bigtiff = ifh[2] == 43
+ self.group = group
+ self.tagtype: dict[int, int] = {}
+ """ Dictionary of tag types """
+ self.reset()
+ self.next = (
+ self._unpack("Q", ifh[8:])[0]
+ if self._bigtiff
+ else self._unpack("L", ifh[4:])[0]
+ )
+ self._legacy_api = False
+
+ prefix = property(lambda self: self._prefix)
+ offset = property(lambda self: self._offset)
+
+ @property
+ def legacy_api(self) -> bool:
+ return self._legacy_api
+
+ @legacy_api.setter
+ def legacy_api(self, value: bool) -> NoReturn:
+ msg = "Not allowing setting of legacy api"
+ raise Exception(msg)
+
+ def reset(self) -> None:
+ self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
+ self._tags_v2: dict[int, Any] = {} # main tag storage
+ self._tagdata: dict[int, bytes] = {}
+ self.tagtype = {} # added 2008-06-05 by Florian Hoech
+ self._next = None
+ self._offset: int | None = None
+
+ def __str__(self) -> str:
+ return str(dict(self))
+
+ def named(self) -> dict[str, Any]:
+ """
+ :returns: dict of name|key: value
+
+ Returns the complete tag dictionary, with named tags where possible.
+ """
+ return {
+ TiffTags.lookup(code, self.group).name: value
+ for code, value in self.items()
+ }
+
+ def __len__(self) -> int:
+ return len(set(self._tagdata) | set(self._tags_v2))
+
+ def __getitem__(self, tag: int) -> Any:
+ if tag not in self._tags_v2: # unpack on the fly
+ data = self._tagdata[tag]
+ typ = self.tagtype[tag]
+ size, handler = self._load_dispatch[typ]
+ self[tag] = handler(self, data, self.legacy_api) # check type
+ val = self._tags_v2[tag]
+ if self.legacy_api and not isinstance(val, (tuple, bytes)):
+ val = (val,)
+ return val
+
+ def __contains__(self, tag: object) -> bool:
+ return tag in self._tags_v2 or tag in self._tagdata
+
+ def __setitem__(self, tag: int, value: Any) -> None:
+ self._setitem(tag, value, self.legacy_api)
+
+ def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None:
+ basetypes = (Number, bytes, str)
+
+ info = TiffTags.lookup(tag, self.group)
+ values = [value] if isinstance(value, basetypes) else value
+
+ if tag not in self.tagtype:
+ if info.type:
+ self.tagtype[tag] = info.type
+ else:
+ self.tagtype[tag] = TiffTags.UNDEFINED
+ if all(isinstance(v, IFDRational) for v in values):
+ for v in values:
+ assert isinstance(v, IFDRational)
+ if v < 0:
+ self.tagtype[tag] = TiffTags.SIGNED_RATIONAL
+ break
+ else:
+ self.tagtype[tag] = TiffTags.RATIONAL
+ elif all(isinstance(v, int) for v in values):
+ short = True
+ signed_short = True
+ long = True
+ for v in values:
+ assert isinstance(v, int)
+ if short and not (0 <= v < 2**16):
+ short = False
+ if signed_short and not (-(2**15) < v < 2**15):
+ signed_short = False
+ if long and v < 0:
+ long = False
+ if short:
+ self.tagtype[tag] = TiffTags.SHORT
+ elif signed_short:
+ self.tagtype[tag] = TiffTags.SIGNED_SHORT
+ elif long:
+ self.tagtype[tag] = TiffTags.LONG
+ else:
+ self.tagtype[tag] = TiffTags.SIGNED_LONG
+ elif all(isinstance(v, float) for v in values):
+ self.tagtype[tag] = TiffTags.DOUBLE
+ elif all(isinstance(v, str) for v in values):
+ self.tagtype[tag] = TiffTags.ASCII
+ elif all(isinstance(v, bytes) for v in values):
+ self.tagtype[tag] = TiffTags.BYTE
+
+ if self.tagtype[tag] == TiffTags.UNDEFINED:
+ values = [
+ v.encode("ascii", "replace") if isinstance(v, str) else v
+ for v in values
+ ]
+ elif self.tagtype[tag] == TiffTags.RATIONAL:
+ values = [float(v) if isinstance(v, int) else v for v in values]
+
+ is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)
+ if not is_ifd:
+ values = tuple(
+ info.cvt_enum(value) if isinstance(value, str) else value
+ for value in values
+ )
+
+ dest = self._tags_v1 if legacy_api else self._tags_v2
+
+ # Three branches:
+ # Spec'd length == 1, Actual length 1, store as element
+ # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.
+ # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.
+ # Don't mess with the legacy api, since it's frozen.
+ if not is_ifd and (
+ (info.length == 1)
+ or self.tagtype[tag] == TiffTags.BYTE
+ or (info.length is None and len(values) == 1 and not legacy_api)
+ ):
+ # Don't mess with the legacy api, since it's frozen.
+ if legacy_api and self.tagtype[tag] in [
+ TiffTags.RATIONAL,
+ TiffTags.SIGNED_RATIONAL,
+ ]: # rationals
+ values = (values,)
+ try:
+ (dest[tag],) = values
+ except ValueError:
+ # We've got a builtin tag with 1 expected entry
+ warnings.warn(
+ f"Metadata Warning, tag {tag} had too many entries: "
+ f"{len(values)}, expected 1"
+ )
+ dest[tag] = values[0]
+
+ else:
+ # Spec'd length > 1 or undefined
+ # Unspec'd, and length > 1
+ dest[tag] = values
+
+ def __delitem__(self, tag: int) -> None:
+ self._tags_v2.pop(tag, None)
+ self._tags_v1.pop(tag, None)
+ self._tagdata.pop(tag, None)
+
+ def __iter__(self) -> Iterator[int]:
+ return iter(set(self._tagdata) | set(self._tags_v2))
+
+ def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]:
+ return struct.unpack(self._endian + fmt, data)
+
+ def _pack(self, fmt: str, *values: Any) -> bytes:
+ return struct.pack(self._endian + fmt, *values)
+
+ list(
+ map(
+ _register_basic,
+ [
+ (TiffTags.SHORT, "H", "short"),
+ (TiffTags.LONG, "L", "long"),
+ (TiffTags.SIGNED_BYTE, "b", "signed byte"),
+ (TiffTags.SIGNED_SHORT, "h", "signed short"),
+ (TiffTags.SIGNED_LONG, "l", "signed long"),
+ (TiffTags.FLOAT, "f", "float"),
+ (TiffTags.DOUBLE, "d", "double"),
+ (TiffTags.IFD, "L", "long"),
+ (TiffTags.LONG8, "Q", "long8"),
+ ],
+ )
+ )
+
+ @_register_loader(1, 1) # Basic type, except for the legacy API.
+ def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes:
+ return data
+
+ @_register_writer(1) # Basic type, except for the legacy API.
+ def write_byte(self, data: bytes | int | IFDRational) -> bytes:
+ if isinstance(data, IFDRational):
+ data = int(data)
+ if isinstance(data, int):
+ data = bytes((data,))
+ return data
+
+ @_register_loader(2, 1)
+ def load_string(self, data: bytes, legacy_api: bool = True) -> str:
+ if data.endswith(b"\0"):
+ data = data[:-1]
+ return data.decode("latin-1", "replace")
+
+ @_register_writer(2)
+ def write_string(self, value: str | bytes | int) -> bytes:
+ # remerge of https://github.com/python-pillow/Pillow/pull/1416
+ if isinstance(value, int):
+ value = str(value)
+ if not isinstance(value, bytes):
+ value = value.encode("ascii", "replace")
+ return value + b"\0"
+
+ @_register_loader(5, 8)
+ def load_rational(
+ self, data: bytes, legacy_api: bool = True
+ ) -> tuple[tuple[int, int] | IFDRational, ...]:
+ vals = self._unpack(f"{len(data) // 4}L", data)
+
+ def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
+ return (a, b) if legacy_api else IFDRational(a, b)
+
+ return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
+
+ @_register_writer(5)
+ def write_rational(self, *values: IFDRational) -> bytes:
+ return b"".join(
+ self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
+ )
+
+ @_register_loader(7, 1)
+ def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes:
+ return data
+
+ @_register_writer(7)
+ def write_undefined(self, value: bytes | int | IFDRational) -> bytes:
+ if isinstance(value, IFDRational):
+ value = int(value)
+ if isinstance(value, int):
+ value = str(value).encode("ascii", "replace")
+ return value
+
+ @_register_loader(10, 8)
+ def load_signed_rational(
+ self, data: bytes, legacy_api: bool = True
+ ) -> tuple[tuple[int, int] | IFDRational, ...]:
+ vals = self._unpack(f"{len(data) // 4}l", data)
+
+ def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
+ return (a, b) if legacy_api else IFDRational(a, b)
+
+ return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
+
+ @_register_writer(10)
+ def write_signed_rational(self, *values: IFDRational) -> bytes:
+ return b"".join(
+ self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
+ for frac in values
+ )
+
+ def _ensure_read(self, fp: IO[bytes], size: int) -> bytes:
+ ret = fp.read(size)
+ if len(ret) != size:
+ msg = (
+ "Corrupt EXIF data. "
+ f"Expecting to read {size} bytes but only got {len(ret)}. "
+ )
+ raise OSError(msg)
+ return ret
+
+ def load(self, fp: IO[bytes]) -> None:
+ self.reset()
+ self._offset = fp.tell()
+
+ try:
+ tag_count = (
+ self._unpack("Q", self._ensure_read(fp, 8))
+ if self._bigtiff
+ else self._unpack("H", self._ensure_read(fp, 2))
+ )[0]
+ for i in range(tag_count):
+ tag, typ, count, data = (
+ self._unpack("HHQ8s", self._ensure_read(fp, 20))
+ if self._bigtiff
+ else self._unpack("HHL4s", self._ensure_read(fp, 12))
+ )
+
+ tagname = TiffTags.lookup(tag, self.group).name
+ typname = TYPES.get(typ, "unknown")
+ msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"
+
+ try:
+ unit_size, handler = self._load_dispatch[typ]
+ except KeyError:
+ logger.debug("%s - unsupported type %s", msg, typ)
+ continue # ignore unsupported type
+ size = count * unit_size
+ if size > (8 if self._bigtiff else 4):
+ here = fp.tell()
+ (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
+ msg += f" Tag Location: {here} - Data Location: {offset}"
+ fp.seek(offset)
+ data = ImageFile._safe_read(fp, size)
+ fp.seek(here)
+ else:
+ data = data[:size]
+
+ if len(data) != size:
+ warnings.warn(
+ "Possibly corrupt EXIF data. "
+ f"Expecting to read {size} bytes but only got {len(data)}."
+ f" Skipping tag {tag}"
+ )
+ logger.debug(msg)
+ continue
+
+ if not data:
+ logger.debug(msg)
+ continue
+
+ self._tagdata[tag] = data
+ self.tagtype[tag] = typ
+
+ msg += " - value: "
+ msg += f"" if size > 32 else repr(data)
+
+ logger.debug(msg)
+
+ (self.next,) = (
+ self._unpack("Q", self._ensure_read(fp, 8))
+ if self._bigtiff
+ else self._unpack("L", self._ensure_read(fp, 4))
+ )
+ except OSError as msg:
+ warnings.warn(str(msg))
+ return
+
+ def _get_ifh(self) -> bytes:
+ ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
+ if self._bigtiff:
+ ifh += self._pack("HH", 8, 0)
+ ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
+
+ return ifh
+
+ def tobytes(self, offset: int = 0) -> bytes:
+ # FIXME What about tagdata?
+ result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
+
+ entries: list[tuple[int, int, int, bytes, bytes]] = []
+
+ fmt = "Q" if self._bigtiff else "L"
+ fmt_size = 8 if self._bigtiff else 4
+ offset += (
+ len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
+ )
+ stripoffsets = None
+
+ # pass 1: convert tags to binary format
+ # always write tags in ascending order
+ for tag, value in sorted(self._tags_v2.items()):
+ if tag == STRIPOFFSETS:
+ stripoffsets = len(entries)
+ typ = self.tagtype[tag]
+ logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
+ is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
+ if is_ifd:
+ ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
+ values = self._tags_v2[tag]
+ for ifd_tag, ifd_value in values.items():
+ ifd[ifd_tag] = ifd_value
+ data = ifd.tobytes(offset)
+ else:
+ values = value if isinstance(value, tuple) else (value,)
+ data = self._write_dispatch[typ](self, *values)
+
+ tagname = TiffTags.lookup(tag, self.group).name
+ typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
+ msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: "
+ msg += f"" if len(data) >= 16 else str(values)
+ logger.debug(msg)
+
+ # count is sum of lengths for string and arbitrary data
+ if is_ifd:
+ count = 1
+ elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:
+ count = len(data)
+ else:
+ count = len(values)
+ # figure out if data fits into the entry
+ if len(data) <= fmt_size:
+ entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
+ else:
+ entries.append((tag, typ, count, self._pack(fmt, offset), data))
+ offset += (len(data) + 1) // 2 * 2 # pad to word
+
+ # update strip offset data to point beyond auxiliary data
+ if stripoffsets is not None:
+ tag, typ, count, value, data = entries[stripoffsets]
+ if data:
+ size, handler = self._load_dispatch[typ]
+ values = [val + offset for val in handler(self, data, self.legacy_api)]
+ data = self._write_dispatch[typ](self, *values)
+ else:
+ value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
+ entries[stripoffsets] = tag, typ, count, value, data
+
+ # pass 2: write entries to file
+ for tag, typ, count, value, data in entries:
+ logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
+ result += self._pack(
+ "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
+ )
+
+ # -- overwrite here for multi-page --
+ result += self._pack(fmt, 0) # end of entries
+
+ # pass 3: write auxiliary data to file
+ for tag, typ, count, value, data in entries:
+ result += data
+ if len(data) & 1:
+ result += b"\0"
+
+ return result
+
+ def save(self, fp: IO[bytes]) -> int:
+ if fp.tell() == 0: # skip TIFF header on subsequent pages
+ fp.write(self._get_ifh())
+
+ offset = fp.tell()
+ result = self.tobytes(offset)
+ fp.write(result)
+ return offset + len(result)
+
+
+ImageFileDirectory_v2._load_dispatch = _load_dispatch
+ImageFileDirectory_v2._write_dispatch = _write_dispatch
+for idx, name in TYPES.items():
+ name = name.replace(" ", "_")
+ setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1])
+ setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx])
+del _load_dispatch, _write_dispatch, idx, name
+
+
+# Legacy ImageFileDirectory support.
+class ImageFileDirectory_v1(ImageFileDirectory_v2):
+ """This class represents the **legacy** interface to a TIFF tag directory.
+
+ Exposes a dictionary interface of the tags in the directory::
+
+ ifd = ImageFileDirectory_v1()
+ ifd[key] = 'Some Data'
+ ifd.tagtype[key] = TiffTags.ASCII
+ print(ifd[key])
+ ('Some Data',)
+
+ Also contains a dictionary of tag types as read from the tiff image file,
+ :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.
+
+ Values are returned as a tuple.
+
+ .. deprecated:: 3.0.0
+ """
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
+ self._legacy_api = True
+
+ tags = property(lambda self: self._tags_v1)
+ tagdata = property(lambda self: self._tagdata)
+
+ # defined in ImageFileDirectory_v2
+ tagtype: dict[int, int]
+ """Dictionary of tag types"""
+
+ @classmethod
+ def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1:
+ """Returns an
+ :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
+ instance with the same data as is contained in the original
+ :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
+ instance.
+
+ :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
+
+ """
+
+ ifd = cls(prefix=original.prefix)
+ ifd._tagdata = original._tagdata
+ ifd.tagtype = original.tagtype
+ ifd.next = original.next # an indicator for multipage tiffs
+ return ifd
+
+ def to_v2(self) -> ImageFileDirectory_v2:
+ """Returns an
+ :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
+ instance with the same data as is contained in the original
+ :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
+ instance.
+
+ :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
+
+ """
+
+ ifd = ImageFileDirectory_v2(prefix=self.prefix)
+ ifd._tagdata = dict(self._tagdata)
+ ifd.tagtype = dict(self.tagtype)
+ ifd._tags_v2 = dict(self._tags_v2)
+ return ifd
+
+ def __contains__(self, tag: object) -> bool:
+ return tag in self._tags_v1 or tag in self._tagdata
+
+ def __len__(self) -> int:
+ return len(set(self._tagdata) | set(self._tags_v1))
+
+ def __iter__(self) -> Iterator[int]:
+ return iter(set(self._tagdata) | set(self._tags_v1))
+
+ def __setitem__(self, tag: int, value: Any) -> None:
+ for legacy_api in (False, True):
+ self._setitem(tag, value, legacy_api)
+
+ def __getitem__(self, tag: int) -> Any:
+ if tag not in self._tags_v1: # unpack on the fly
+ data = self._tagdata[tag]
+ typ = self.tagtype[tag]
+ size, handler = self._load_dispatch[typ]
+ for legacy in (False, True):
+ self._setitem(tag, handler(self, data, legacy), legacy)
+ val = self._tags_v1[tag]
+ if not isinstance(val, (tuple, bytes)):
+ val = (val,)
+ return val
+
+
+# undone -- switch this pointer
+ImageFileDirectory = ImageFileDirectory_v1
+
+
+##
+# Image plugin for TIFF files.
+
+
+class TiffImageFile(ImageFile.ImageFile):
+ format = "TIFF"
+ format_description = "Adobe TIFF"
+ _close_exclusive_fp_after_loading = False
+
+ def __init__(
+ self,
+ fp: StrOrBytesPath | IO[bytes],
+ filename: str | bytes | None = None,
+ ) -> None:
+ self.tag_v2: ImageFileDirectory_v2
+ """ Image file directory (tag dictionary) """
+
+ self.tag: ImageFileDirectory_v1
+ """ Legacy tag entries """
+
+ super().__init__(fp, filename)
+
+ def _open(self) -> None:
+ """Open the first image in a TIFF file"""
+
+ # Header
+ assert self.fp is not None
+ ifh = self.fp.read(8)
+ if ifh[2] == 43:
+ ifh += self.fp.read(8)
+
+ self.tag_v2 = ImageFileDirectory_v2(ifh)
+
+ # setup frame pointers
+ self.__first = self.__next = self.tag_v2.next
+ self.__frame = -1
+ self._fp = self.fp
+ self._frame_pos: list[int] = []
+ self._n_frames: int | None = None
+
+ logger.debug("*** TiffImageFile._open ***")
+ logger.debug("- __first: %s", self.__first)
+ logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes)
+
+ # and load the first frame
+ self._seek(0)
+
+ @property
+ def n_frames(self) -> int:
+ current_n_frames = self._n_frames
+ if current_n_frames is None:
+ current = self.tell()
+ self._seek(len(self._frame_pos))
+ while self._n_frames is None:
+ self._seek(self.tell() + 1)
+ self.seek(current)
+ assert self._n_frames is not None
+ return self._n_frames
+
+ def seek(self, frame: int) -> None:
+ """Select a given frame as current image"""
+ if not self._seek_check(frame):
+ return
+ self._seek(frame)
+ if self._im is not None and (
+ self.im.size != self._tile_size
+ or self.im.mode != self.mode
+ or self.readonly
+ ):
+ self._im = None
+
+ def _seek(self, frame: int) -> None:
+ if isinstance(self._fp, DeferredError):
+ raise self._fp.ex
+ self.fp = self._fp
+
+ while len(self._frame_pos) <= frame:
+ if not self.__next:
+ msg = "no more images in TIFF file"
+ raise EOFError(msg)
+ logger.debug(
+ "Seeking to frame %s, on frame %s, __next %s, location: %s",
+ frame,
+ self.__frame,
+ self.__next,
+ self.fp.tell(),
+ )
+ if self.__next >= 2**63:
+ msg = "Unable to seek to frame"
+ raise ValueError(msg)
+ self.fp.seek(self.__next)
+ self._frame_pos.append(self.__next)
+ logger.debug("Loading tags, location: %s", self.fp.tell())
+ self.tag_v2.load(self.fp)
+ if self.tag_v2.next in self._frame_pos:
+ # This IFD has already been processed
+ # Declare this to be the end of the image
+ self.__next = 0
+ else:
+ self.__next = self.tag_v2.next
+ if self.__next == 0:
+ self._n_frames = frame + 1
+ if len(self._frame_pos) == 1:
+ self.is_animated = self.__next != 0
+ self.__frame += 1
+ self.fp.seek(self._frame_pos[frame])
+ self.tag_v2.load(self.fp)
+ if XMP in self.tag_v2:
+ xmp = self.tag_v2[XMP]
+ if isinstance(xmp, tuple) and len(xmp) == 1:
+ xmp = xmp[0]
+ self.info["xmp"] = xmp
+ elif "xmp" in self.info:
+ del self.info["xmp"]
+ self._reload_exif()
+ # fill the legacy tag/ifd entries
+ self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
+ self.__frame = frame
+ self._setup()
+
+ def tell(self) -> int:
+ """Return the current frame number"""
+ return self.__frame
+
+ def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]:
+ """
+ Returns a dictionary of Photoshop "Image Resource Blocks".
+ The keys are the image resource ID. For more information, see
+ https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727
+
+ :returns: Photoshop "Image Resource Blocks" in a dictionary.
+ """
+ blocks = {}
+ val = self.tag_v2.get(ExifTags.Base.ImageResources)
+ if val:
+ while val.startswith(b"8BIM") and len(val) >= 12:
+ id = i16(val[4:6])
+ n = math.ceil((val[6] + 1) / 2) * 2
+ try:
+ size = i32(val[6 + n : 10 + n])
+ except struct.error:
+ break
+ data = val[10 + n : 10 + n + size]
+ blocks[id] = {"data": data}
+
+ val = val[math.ceil((10 + n + size) / 2) * 2 :]
+ return blocks
+
+ def load(self) -> Image.core.PixelAccess | None:
+ if self.tile and self.use_load_libtiff:
+ return self._load_libtiff()
+ return super().load()
+
+ def load_prepare(self) -> None:
+ if self._im is None:
+ Image._decompression_bomb_check(self._tile_size)
+ self.im = Image.core.new(self.mode, self._tile_size)
+ ImageFile.ImageFile.load_prepare(self)
+
+ def load_end(self) -> None:
+ # allow closing if we're on the first frame, there's no next
+ # This is the ImageFile.load path only, libtiff specific below.
+ if not self.is_animated:
+ self._close_exclusive_fp_after_loading = True
+
+ # load IFD data from fp before it is closed
+ exif = self.getexif()
+ for key in TiffTags.TAGS_V2_GROUPS:
+ if key not in exif:
+ continue
+ exif.get_ifd(key)
+
+ ImageOps.exif_transpose(self, in_place=True)
+ if ExifTags.Base.Orientation in self.tag_v2:
+ del self.tag_v2[ExifTags.Base.Orientation]
+
+ def _load_libtiff(self) -> Image.core.PixelAccess | None:
+ """Overload method triggered when we detect a compressed tiff
+ Calls out to libtiff"""
+
+ Image.Image.load(self)
+
+ self.load_prepare()
+
+ if not len(self.tile) == 1:
+ msg = "Not exactly one tile"
+ raise OSError(msg)
+
+ # (self._compression, (extents tuple),
+ # 0, (rawmode, self._compression, fp))
+ extents = self.tile[0][1]
+ args = self.tile[0][3]
+
+ # To be nice on memory footprint, if there's a
+ # file descriptor, use that instead of reading
+ # into a string in python.
+ assert self.fp is not None
+ try:
+ fp = hasattr(self.fp, "fileno") and self.fp.fileno()
+ # flush the file descriptor, prevents error on pypy 2.4+
+ # should also eliminate the need for fp.tell
+ # in _seek
+ if hasattr(self.fp, "flush"):
+ self.fp.flush()
+ except OSError:
+ # io.BytesIO have a fileno, but returns an OSError if
+ # it doesn't use a file descriptor.
+ fp = False
+
+ if fp:
+ assert isinstance(args, tuple)
+ args_list = list(args)
+ args_list[2] = fp
+ args = tuple(args_list)
+
+ decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig)
+ try:
+ decoder.setimage(self.im, extents)
+ except ValueError as e:
+ msg = "Couldn't set the image"
+ raise OSError(msg) from e
+
+ close_self_fp = self._exclusive_fp and not self.is_animated
+ if hasattr(self.fp, "getvalue"):
+ # We've got a stringio like thing passed in. Yay for all in memory.
+ # The decoder needs the entire file in one shot, so there's not
+ # a lot we can do here other than give it the entire file.
+ # unless we could do something like get the address of the
+ # underlying string for stringio.
+ #
+ # Rearranging for supporting byteio items, since they have a fileno
+ # that returns an OSError if there's no underlying fp. Easier to
+ # deal with here by reordering.
+ logger.debug("have getvalue. just sending in a string from getvalue")
+ n, err = decoder.decode(self.fp.getvalue())
+ elif fp:
+ # we've got a actual file on disk, pass in the fp.
+ logger.debug("have fileno, calling fileno version of the decoder.")
+ if not close_self_fp:
+ self.fp.seek(0)
+ # Save and restore the file position, because libtiff will move it
+ # outside of the Python runtime, and that will confuse
+ # io.BufferedReader and possible others.
+ # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(),
+ # because the buffer read head already may not equal the actual
+ # file position, and fp.seek() may just adjust it's internal
+ # pointer and not actually seek the OS file handle.
+ pos = os.lseek(fp, 0, os.SEEK_CUR)
+ # 4 bytes, otherwise the trace might error out
+ n, err = decoder.decode(b"fpfp")
+ os.lseek(fp, pos, os.SEEK_SET)
+ else:
+ # we have something else.
+ logger.debug("don't have fileno or getvalue. just reading")
+ self.fp.seek(0)
+ # UNDONE -- so much for that buffer size thing.
+ n, err = decoder.decode(self.fp.read())
+
+ self.tile = []
+ self.readonly = 0
+
+ self.load_end()
+
+ if close_self_fp:
+ self.fp.close()
+ self.fp = None # might be shared
+
+ if err < 0:
+ msg = f"decoder error {err}"
+ raise OSError(msg)
+
+ return Image.Image.load(self)
+
+ def _setup(self) -> None:
+ """Setup this image object based on current tags"""
+
+ if 0xBC01 in self.tag_v2:
+ msg = "Windows Media Photo files not yet supported"
+ raise OSError(msg)
+
+ # extract relevant tags
+ self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
+ self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)
+
+ # photometric is a required tag, but not everyone is reading
+ # the specification
+ photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)
+
+ # old style jpeg compression images most certainly are YCbCr
+ if self._compression == "tiff_jpeg":
+ photo = 6
+
+ fillorder = self.tag_v2.get(FILLORDER, 1)
+
+ logger.debug("*** Summary ***")
+ logger.debug("- compression: %s", self._compression)
+ logger.debug("- photometric_interpretation: %s", photo)
+ logger.debug("- planar_configuration: %s", self._planar_configuration)
+ logger.debug("- fill_order: %s", fillorder)
+ logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
+
+ # size
+ try:
+ xsize = self.tag_v2[IMAGEWIDTH]
+ ysize = self.tag_v2[IMAGELENGTH]
+ except KeyError as e:
+ msg = "Missing dimensions"
+ raise TypeError(msg) from e
+ if not isinstance(xsize, int) or not isinstance(ysize, int):
+ msg = "Invalid dimensions"
+ raise ValueError(msg)
+ self._tile_size = xsize, ysize
+ orientation = self.tag_v2.get(ExifTags.Base.Orientation)
+ if orientation in (5, 6, 7, 8):
+ self._size = ysize, xsize
+ else:
+ self._size = xsize, ysize
+
+ logger.debug("- size: %s", self.size)
+
+ sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
+ if len(sample_format) > 1 and max(sample_format) == min(sample_format):
+ # SAMPLEFORMAT is properly per band, so an RGB image will
+ # be (1,1,1). But, we don't support per band pixel types,
+ # and anything more than one band is a uint8. So, just
+ # take the first element. Revisit this if adding support
+ # for more exotic images.
+ sample_format = (sample_format[0],)
+
+ bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
+ extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
+ samples_per_pixel = self.tag_v2.get(
+ SAMPLESPERPIXEL,
+ 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
+ )
+ if photo in (2, 6, 8): # RGB, YCbCr, LAB
+ bps_count = 3
+ elif photo == 5: # CMYK
+ bps_count = 4
+ else:
+ bps_count = 1
+ if self._planar_configuration == 2 and extra_tuple and max(extra_tuple) == 0:
+ # If components are stored separately,
+ # then unspecified extra components at the end can be ignored
+ bps_tuple = bps_tuple[: -len(extra_tuple)]
+ samples_per_pixel -= len(extra_tuple)
+ extra_tuple = ()
+ bps_count += len(extra_tuple)
+ bps_actual_count = len(bps_tuple)
+
+ if samples_per_pixel > MAX_SAMPLESPERPIXEL:
+ # DOS check, samples_per_pixel can be a Long, and we extend the tuple below
+ logger.error(
+ "More samples per pixel than can be decoded: %s", samples_per_pixel
+ )
+ msg = "Invalid value for samples per pixel"
+ raise SyntaxError(msg)
+
+ if samples_per_pixel < bps_actual_count:
+ # If a file has more values in bps_tuple than expected,
+ # remove the excess.
+ bps_tuple = bps_tuple[:samples_per_pixel]
+ elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:
+ # If a file has only one value in bps_tuple, when it should have more,
+ # presume it is the same number of bits for all of the samples.
+ bps_tuple = bps_tuple * samples_per_pixel
+
+ if len(bps_tuple) != samples_per_pixel:
+ msg = "unknown data organization"
+ raise SyntaxError(msg)
+
+ # mode: check photometric interpretation and bits per pixel
+ key = (
+ self.tag_v2.prefix,
+ photo,
+ sample_format,
+ fillorder,
+ bps_tuple,
+ extra_tuple,
+ )
+ logger.debug("format key: %s", key)
+ try:
+ self._mode, rawmode = OPEN_INFO[key]
+ except KeyError as e:
+ logger.debug("- unsupported format")
+ msg = "unknown pixel mode"
+ raise SyntaxError(msg) from e
+
+ logger.debug("- raw mode: %s", rawmode)
+ logger.debug("- pil mode: %s", self.mode)
+
+ self.info["compression"] = self._compression
+
+ xres = self.tag_v2.get(X_RESOLUTION, 1)
+ yres = self.tag_v2.get(Y_RESOLUTION, 1)
+
+ if xres and yres:
+ resunit = self.tag_v2.get(RESOLUTION_UNIT)
+ if resunit == 2: # dots per inch
+ self.info["dpi"] = (xres, yres)
+ elif resunit == 3: # dots per centimeter. convert to dpi
+ self.info["dpi"] = (xres * 2.54, yres * 2.54)
+ elif resunit is None: # used to default to 1, but now 2)
+ self.info["dpi"] = (xres, yres)
+ # For backward compatibility,
+ # we also preserve the old behavior
+ self.info["resolution"] = xres, yres
+ else: # No absolute unit of measurement
+ self.info["resolution"] = xres, yres
+
+ # build tile descriptors
+ x = y = layer = 0
+ self.tile = []
+ self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw"
+ if self.use_load_libtiff:
+ # Decoder expects entire file as one tile.
+ # There's a buffer size limit in load (64k)
+ # so large g4 images will fail if we use that
+ # function.
+ #
+ # Setup the one tile for the whole image, then
+ # use the _load_libtiff function.
+
+ # libtiff handles the fillmode for us, so 1;IR should
+ # actually be 1;I. Including the R double reverses the
+ # bits, so stripes of the image are reversed. See
+ # https://github.com/python-pillow/Pillow/issues/279
+ if fillorder == 2:
+ # Replace fillorder with fillorder=1
+ key = key[:3] + (1,) + key[4:]
+ logger.debug("format key: %s", key)
+ # this should always work, since all the
+ # fillorder==2 modes have a corresponding
+ # fillorder=1 mode
+ self._mode, rawmode = OPEN_INFO[key]
+ # YCbCr images with new jpeg compression with pixels in one plane
+ # unpacked straight into RGB values
+ if (
+ photo == 6
+ and self._compression == "jpeg"
+ and self._planar_configuration == 1
+ ):
+ rawmode = "RGB"
+ # libtiff always returns the bytes in native order.
+ # we're expecting image byte order. So, if the rawmode
+ # contains I;16, we need to convert from native to image
+ # byte order.
+ elif rawmode == "I;16":
+ rawmode = "I;16N"
+ elif rawmode.endswith((";16B", ";16L")):
+ rawmode = rawmode[:-1] + "N"
+
+ # Offset in the tile tuple is 0, we go from 0,0 to
+ # w,h, and we only do this once -- eds
+ a = (rawmode, self._compression, False, self.tag_v2.offset)
+ self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a))
+
+ elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
+ # striped image
+ if STRIPOFFSETS in self.tag_v2:
+ offsets = self.tag_v2[STRIPOFFSETS]
+ h = self.tag_v2.get(ROWSPERSTRIP, ysize)
+ w = xsize
+ else:
+ # tiled image
+ offsets = self.tag_v2[TILEOFFSETS]
+ tilewidth = self.tag_v2.get(TILEWIDTH)
+ h = self.tag_v2.get(TILELENGTH)
+ if not isinstance(tilewidth, int) or not isinstance(h, int):
+ msg = "Invalid tile dimensions"
+ raise ValueError(msg)
+ w = tilewidth
+
+ if w == xsize and h == ysize and self._planar_configuration != 2:
+ # Every tile covers the image. Only use the last offset
+ offsets = offsets[-1:]
+
+ for offset in offsets:
+ if x + w > xsize:
+ stride = w * sum(bps_tuple) / 8 # bytes per line
+ else:
+ stride = 0
+
+ tile_rawmode = rawmode
+ if self._planar_configuration == 2:
+ # each band on it's own layer
+ tile_rawmode = rawmode[layer]
+ # adjust stride width accordingly
+ stride /= bps_count
+
+ args = (tile_rawmode, int(stride), 1)
+ self.tile.append(
+ ImageFile._Tile(
+ self._compression,
+ (x, y, min(x + w, xsize), min(y + h, ysize)),
+ offset,
+ args,
+ )
+ )
+ x += w
+ if x >= xsize:
+ x, y = 0, y + h
+ if y >= ysize:
+ y = 0
+ layer += 1
+ else:
+ logger.debug("- unsupported data organization")
+ msg = "unknown data organization"
+ raise SyntaxError(msg)
+
+ # Fix up info.
+ if ICCPROFILE in self.tag_v2:
+ self.info["icc_profile"] = self.tag_v2[ICCPROFILE]
+
+ # fixup palette descriptor
+
+ if self.mode in ["P", "PA"]:
+ palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
+ self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
+
+
+#
+# --------------------------------------------------------------------
+# Write TIFF files
+
+# little endian is default except for image modes with
+# explicit big endian byte-order
+
+SAVE_INFO = {
+ # mode => rawmode, byteorder, photometrics,
+ # sampleformat, bitspersample, extra
+ "1": ("1", II, 1, 1, (1,), None),
+ "L": ("L", II, 1, 1, (8,), None),
+ "LA": ("LA", II, 1, 1, (8, 8), 2),
+ "P": ("P", II, 3, 1, (8,), None),
+ "PA": ("PA", II, 3, 1, (8, 8), 2),
+ "I": ("I;32S", II, 1, 2, (32,), None),
+ "I;16": ("I;16", II, 1, 1, (16,), None),
+ "I;16L": ("I;16L", II, 1, 1, (16,), None),
+ "F": ("F;32F", II, 1, 3, (32,), None),
+ "RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
+ "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
+ "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
+ "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
+ "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
+ "LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
+ "I;16B": ("I;16B", MM, 1, 1, (16,), None),
+}
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ try:
+ rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
+ except KeyError as e:
+ msg = f"cannot write mode {im.mode} as TIFF"
+ raise OSError(msg) from e
+
+ encoderinfo = im.encoderinfo
+ encoderconfig = im.encoderconfig
+
+ ifd = ImageFileDirectory_v2(prefix=prefix)
+ if encoderinfo.get("big_tiff"):
+ ifd._bigtiff = True
+
+ try:
+ compression = encoderinfo["compression"]
+ except KeyError:
+ compression = im.info.get("compression")
+ if isinstance(compression, int):
+ # compression value may be from BMP. Ignore it
+ compression = None
+ if compression is None:
+ compression = "raw"
+ elif compression == "tiff_jpeg":
+ # OJPEG is obsolete, so use new-style JPEG compression instead
+ compression = "jpeg"
+ elif compression == "tiff_deflate":
+ compression = "tiff_adobe_deflate"
+
+ libtiff = WRITE_LIBTIFF or compression != "raw"
+
+ # required for color libtiff images
+ ifd[PLANAR_CONFIGURATION] = 1
+
+ ifd[IMAGEWIDTH] = im.size[0]
+ ifd[IMAGELENGTH] = im.size[1]
+
+ # write any arbitrary tags passed in as an ImageFileDirectory
+ if "tiffinfo" in encoderinfo:
+ info = encoderinfo["tiffinfo"]
+ elif "exif" in encoderinfo:
+ info = encoderinfo["exif"]
+ if isinstance(info, bytes):
+ exif = Image.Exif()
+ exif.load(info)
+ info = exif
+ else:
+ info = {}
+ logger.debug("Tiffinfo Keys: %s", list(info))
+ if isinstance(info, ImageFileDirectory_v1):
+ info = info.to_v2()
+ for key in info:
+ if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS:
+ ifd[key] = info.get_ifd(key)
+ else:
+ ifd[key] = info.get(key)
+ try:
+ ifd.tagtype[key] = info.tagtype[key]
+ except Exception:
+ pass # might not be an IFD. Might not have populated type
+
+ legacy_ifd = {}
+ if hasattr(im, "tag"):
+ legacy_ifd = im.tag.to_v2()
+
+ supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
+ if supplied_tags.get(PLANAR_CONFIGURATION) == 2 and EXTRASAMPLES in supplied_tags:
+ # If the image used separate component planes,
+ # then EXTRASAMPLES should be ignored when saving contiguously
+ if SAMPLESPERPIXEL in supplied_tags:
+ supplied_tags[SAMPLESPERPIXEL] -= len(supplied_tags[EXTRASAMPLES])
+ del supplied_tags[EXTRASAMPLES]
+ for tag in (
+ # IFD offset that may not be correct in the saved image
+ EXIFIFD,
+ # Determined by the image format and should not be copied from legacy_ifd.
+ SAMPLEFORMAT,
+ ):
+ if tag in supplied_tags:
+ del supplied_tags[tag]
+
+ # additions written by Greg Couch, gregc@cgl.ucsf.edu
+ # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
+ if hasattr(im, "tag_v2"):
+ # preserve tags from original TIFF image file
+ for key in (
+ RESOLUTION_UNIT,
+ X_RESOLUTION,
+ Y_RESOLUTION,
+ IPTC_NAA_CHUNK,
+ PHOTOSHOP_CHUNK,
+ XMP,
+ ):
+ if key in im.tag_v2:
+ if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in (
+ TiffTags.BYTE,
+ TiffTags.UNDEFINED,
+ ):
+ del supplied_tags[key]
+ else:
+ ifd[key] = im.tag_v2[key]
+ ifd.tagtype[key] = im.tag_v2.tagtype[key]
+
+ # preserve ICC profile (should also work when saving other formats
+ # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
+ icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
+ if icc:
+ ifd[ICCPROFILE] = icc
+
+ for key, name in [
+ (IMAGEDESCRIPTION, "description"),
+ (X_RESOLUTION, "resolution"),
+ (Y_RESOLUTION, "resolution"),
+ (X_RESOLUTION, "x_resolution"),
+ (Y_RESOLUTION, "y_resolution"),
+ (RESOLUTION_UNIT, "resolution_unit"),
+ (SOFTWARE, "software"),
+ (DATE_TIME, "date_time"),
+ (ARTIST, "artist"),
+ (COPYRIGHT, "copyright"),
+ ]:
+ if name in encoderinfo:
+ ifd[key] = encoderinfo[name]
+
+ dpi = encoderinfo.get("dpi")
+ if dpi:
+ ifd[RESOLUTION_UNIT] = 2
+ ifd[X_RESOLUTION] = dpi[0]
+ ifd[Y_RESOLUTION] = dpi[1]
+
+ if bits != (1,):
+ ifd[BITSPERSAMPLE] = bits
+ if len(bits) != 1:
+ ifd[SAMPLESPERPIXEL] = len(bits)
+ if extra is not None:
+ ifd[EXTRASAMPLES] = extra
+ if format != 1:
+ ifd[SAMPLEFORMAT] = format
+
+ if PHOTOMETRIC_INTERPRETATION not in ifd:
+ ifd[PHOTOMETRIC_INTERPRETATION] = photo
+ elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
+ if im.mode == "1":
+ inverted_im = im.copy()
+ px = inverted_im.load()
+ if px is not None:
+ for y in range(inverted_im.height):
+ for x in range(inverted_im.width):
+ px[x, y] = 0 if px[x, y] == 255 else 255
+ im = inverted_im
+ else:
+ im = ImageOps.invert(im)
+
+ if im.mode in ["P", "PA"]:
+ lut = im.im.getpalette("RGB", "RGB;L")
+ colormap = []
+ colors = len(lut) // 3
+ for i in range(3):
+ colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]
+ colormap += [0] * (256 - colors)
+ ifd[COLORMAP] = colormap
+ # data orientation
+ w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
+ stride = len(bits) * ((w * bits[0] + 7) // 8)
+ if ROWSPERSTRIP not in ifd:
+ # aim for given strip size (64 KB by default) when using libtiff writer
+ if libtiff:
+ im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
+ rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
+ # JPEG encoder expects multiple of 8 rows
+ if compression == "jpeg":
+ rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
+ else:
+ rows_per_strip = h
+ if rows_per_strip == 0:
+ rows_per_strip = 1
+ ifd[ROWSPERSTRIP] = rows_per_strip
+ strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
+ strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
+ if strip_byte_counts >= 2**16:
+ ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
+ ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
+ stride * h - strip_byte_counts * (strips_per_image - 1),
+ )
+ ifd[STRIPOFFSETS] = tuple(
+ range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
+ ) # this is adjusted by IFD writer
+ # no compression by default:
+ ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
+
+ if im.mode == "YCbCr":
+ for tag, default_value in {
+ YCBCRSUBSAMPLING: (1, 1),
+ REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
+ }.items():
+ ifd.setdefault(tag, default_value)
+
+ blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
+ if libtiff:
+ if "quality" in encoderinfo:
+ quality = encoderinfo["quality"]
+ if not isinstance(quality, int) or quality < 0 or quality > 100:
+ msg = "Invalid quality setting"
+ raise ValueError(msg)
+ if compression != "jpeg":
+ msg = "quality setting only supported for 'jpeg' compression"
+ raise ValueError(msg)
+ ifd[JPEGQUALITY] = quality
+
+ logger.debug("Saving using libtiff encoder")
+ logger.debug("Items: %s", sorted(ifd.items()))
+ _fp = 0
+ if hasattr(fp, "fileno"):
+ try:
+ fp.seek(0)
+ _fp = fp.fileno()
+ except io.UnsupportedOperation:
+ pass
+
+ # optional types for non core tags
+ types = {}
+ # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
+ # based on the data in the strip.
+ # OSUBFILETYPE is deprecated.
+ # The other tags expect arrays with a certain length (fixed or depending on
+ # BITSPERSAMPLE, etc), passing arrays with a different length will result in
+ # segfaults. Block these tags until we add extra validation.
+ # SUBIFD may also cause a segfault.
+ blocklist += [
+ OSUBFILETYPE,
+ REFERENCEBLACKWHITE,
+ STRIPBYTECOUNTS,
+ STRIPOFFSETS,
+ TRANSFERFUNCTION,
+ SUBIFD,
+ ]
+
+ # bits per sample is a single short in the tiff directory, not a list.
+ atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
+ # Merge the ones that we have with (optional) more bits from
+ # the original file, e.g x,y resolution so that we can
+ # save(load('')) == original file.
+ for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
+ # Libtiff can only process certain core items without adding
+ # them to the custom dictionary.
+ # Custom items are supported for int, float, unicode, string and byte
+ # values. Other types and tuples require a tagtype.
+ if tag not in TiffTags.LIBTIFF_CORE:
+ if tag in TiffTags.TAGS_V2_GROUPS:
+ types[tag] = TiffTags.LONG8
+ elif tag in ifd.tagtype:
+ types[tag] = ifd.tagtype[tag]
+ elif isinstance(value, (int, float, str, bytes)) or (
+ isinstance(value, tuple)
+ and all(isinstance(v, (int, float, IFDRational)) for v in value)
+ ):
+ type = TiffTags.lookup(tag).type
+ if type:
+ types[tag] = type
+ if tag not in atts and tag not in blocklist:
+ if isinstance(value, str):
+ atts[tag] = value.encode("ascii", "replace") + b"\0"
+ elif isinstance(value, IFDRational):
+ atts[tag] = float(value)
+ else:
+ atts[tag] = value
+
+ if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
+ atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
+
+ logger.debug("Converted items: %s", sorted(atts.items()))
+
+ # libtiff always expects the bytes in native order.
+ # we're storing image byte order. So, if the rawmode
+ # contains I;16, we need to convert from native to image
+ # byte order.
+ if im.mode in ("I;16", "I;16B", "I;16L"):
+ rawmode = "I;16N"
+
+ # Pass tags as sorted list so that the tags are set in a fixed order.
+ # This is required by libtiff for some tags. For example, the JPEGQUALITY
+ # pseudo tag requires that the COMPRESS tag was already set.
+ tags = list(atts.items())
+ tags.sort()
+ a = (rawmode, compression, _fp, filename, tags, types)
+ encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
+ encoder.setimage(im.im, (0, 0) + im.size)
+ while True:
+ errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:]
+ if not _fp:
+ fp.write(data)
+ if errcode:
+ break
+ if errcode < 0:
+ msg = f"encoder error {errcode} when writing image file"
+ raise OSError(msg)
+
+ else:
+ for tag in blocklist:
+ del ifd[tag]
+ offset = ifd.save(fp)
+
+ ImageFile._save(
+ im,
+ fp,
+ [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
+ )
+
+ # -- helper for multi-page save --
+ if "_debug_multipage" in encoderinfo:
+ # just to access o32 and o16 (using correct byte order)
+ setattr(im, "_debug_multipage", ifd)
+
+
+class AppendingTiffWriter(io.BytesIO):
+ fieldSizes = [
+ 0, # None
+ 1, # byte
+ 1, # ascii
+ 2, # short
+ 4, # long
+ 8, # rational
+ 1, # sbyte
+ 1, # undefined
+ 2, # sshort
+ 4, # slong
+ 8, # srational
+ 4, # float
+ 8, # double
+ 4, # ifd
+ 2, # unicode
+ 4, # complex
+ 8, # long8
+ ]
+
+ Tags = {
+ 273, # StripOffsets
+ 288, # FreeOffsets
+ 324, # TileOffsets
+ 519, # JPEGQTables
+ 520, # JPEGDCTables
+ 521, # JPEGACTables
+ }
+
+ def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None:
+ self.f: IO[bytes]
+ if is_path(fn):
+ self.name = fn
+ self.close_fp = True
+ try:
+ self.f = open(fn, "w+b" if new else "r+b")
+ except OSError:
+ self.f = open(fn, "w+b")
+ else:
+ self.f = cast(IO[bytes], fn)
+ self.close_fp = False
+ self.beginning = self.f.tell()
+ self.setup()
+
+ def setup(self) -> None:
+ # Reset everything.
+ self.f.seek(self.beginning, os.SEEK_SET)
+
+ self.whereToWriteNewIFDOffset: int | None = None
+ self.offsetOfNewPage = 0
+
+ self.IIMM = iimm = self.f.read(4)
+ self._bigtiff = b"\x2b" in iimm
+ if not iimm:
+ # empty file - first page
+ self.isFirst = True
+ return
+
+ self.isFirst = False
+ if iimm not in PREFIXES:
+ msg = "Invalid TIFF file header"
+ raise RuntimeError(msg)
+
+ self.setEndian("<" if iimm.startswith(II) else ">")
+
+ if self._bigtiff:
+ self.f.seek(4, os.SEEK_CUR)
+ self.skipIFDs()
+ self.goToEnd()
+
+ def finalize(self) -> None:
+ if self.isFirst:
+ return
+
+ # fix offsets
+ self.f.seek(self.offsetOfNewPage)
+
+ iimm = self.f.read(4)
+ if not iimm:
+ # Make it easy to finish a frame without committing to a new one.
+ return
+
+ if iimm != self.IIMM:
+ msg = "IIMM of new page doesn't match IIMM of first page"
+ raise RuntimeError(msg)
+
+ if self._bigtiff:
+ self.f.seek(4, os.SEEK_CUR)
+ ifd_offset = self._read(8 if self._bigtiff else 4)
+ ifd_offset += self.offsetOfNewPage
+ assert self.whereToWriteNewIFDOffset is not None
+ self.f.seek(self.whereToWriteNewIFDOffset)
+ self._write(ifd_offset, 8 if self._bigtiff else 4)
+ self.f.seek(ifd_offset)
+ self.fixIFD()
+
+ def newFrame(self) -> None:
+ # Call this to finish a frame.
+ self.finalize()
+ self.setup()
+
+ def __enter__(self) -> AppendingTiffWriter:
+ return self
+
+ def __exit__(self, *args: object) -> None:
+ if self.close_fp:
+ self.close()
+
+ def tell(self) -> int:
+ return self.f.tell() - self.offsetOfNewPage
+
+ def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
+ """
+ :param offset: Distance to seek.
+ :param whence: Whether the distance is relative to the start,
+ end or current position.
+ :returns: The resulting position, relative to the start.
+ """
+ if whence == os.SEEK_SET:
+ offset += self.offsetOfNewPage
+
+ self.f.seek(offset, whence)
+ return self.tell()
+
+ def goToEnd(self) -> None:
+ self.f.seek(0, os.SEEK_END)
+ pos = self.f.tell()
+
+ # pad to 16 byte boundary
+ pad_bytes = 16 - pos % 16
+ if 0 < pad_bytes < 16:
+ self.f.write(bytes(pad_bytes))
+ self.offsetOfNewPage = self.f.tell()
+
+ def setEndian(self, endian: str) -> None:
+ self.endian = endian
+ self.longFmt = f"{self.endian}L"
+ self.shortFmt = f"{self.endian}H"
+ self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
+
+ def skipIFDs(self) -> None:
+ while True:
+ ifd_offset = self._read(8 if self._bigtiff else 4)
+ if ifd_offset == 0:
+ self.whereToWriteNewIFDOffset = self.f.tell() - (
+ 8 if self._bigtiff else 4
+ )
+ break
+
+ self.f.seek(ifd_offset)
+ num_tags = self._read(8 if self._bigtiff else 2)
+ self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
+
+ def write(self, data: Buffer, /) -> int:
+ return self.f.write(data)
+
+ def _fmt(self, field_size: int) -> str:
+ try:
+ return {2: "H", 4: "L", 8: "Q"}[field_size]
+ except KeyError:
+ msg = "offset is not supported"
+ raise RuntimeError(msg)
+
+ def _read(self, field_size: int) -> int:
+ (value,) = struct.unpack(
+ self.endian + self._fmt(field_size), self.f.read(field_size)
+ )
+ return value
+
+ def readShort(self) -> int:
+ return self._read(2)
+
+ def readLong(self) -> int:
+ return self._read(4)
+
+ @staticmethod
+ def _verify_bytes_written(bytes_written: int | None, expected: int) -> None:
+ if bytes_written is not None and bytes_written != expected:
+ msg = f"wrote only {bytes_written} bytes but wanted {expected}"
+ raise RuntimeError(msg)
+
+ def _rewriteLast(
+ self, value: int, field_size: int, new_field_size: int = 0
+ ) -> None:
+ self.f.seek(-field_size, os.SEEK_CUR)
+ if not new_field_size:
+ new_field_size = field_size
+ bytes_written = self.f.write(
+ struct.pack(self.endian + self._fmt(new_field_size), value)
+ )
+ self._verify_bytes_written(bytes_written, new_field_size)
+
+ def rewriteLastShortToLong(self, value: int) -> None:
+ self._rewriteLast(value, 2, 4)
+
+ def rewriteLastShort(self, value: int) -> None:
+ return self._rewriteLast(value, 2)
+
+ def rewriteLastLong(self, value: int) -> None:
+ return self._rewriteLast(value, 4)
+
+ def _write(self, value: int, field_size: int) -> None:
+ bytes_written = self.f.write(
+ struct.pack(self.endian + self._fmt(field_size), value)
+ )
+ self._verify_bytes_written(bytes_written, field_size)
+
+ def writeShort(self, value: int) -> None:
+ self._write(value, 2)
+
+ def writeLong(self, value: int) -> None:
+ self._write(value, 4)
+
+ def close(self) -> None:
+ self.finalize()
+ if self.close_fp:
+ self.f.close()
+
+ def fixIFD(self) -> None:
+ num_tags = self._read(8 if self._bigtiff else 2)
+
+ for i in range(num_tags):
+ tag, field_type, count = struct.unpack(
+ self.tagFormat, self.f.read(12 if self._bigtiff else 8)
+ )
+
+ field_size = self.fieldSizes[field_type]
+ total_size = field_size * count
+ fmt_size = 8 if self._bigtiff else 4
+ is_local = total_size <= fmt_size
+ if not is_local:
+ offset = self._read(fmt_size) + self.offsetOfNewPage
+ self._rewriteLast(offset, fmt_size)
+
+ if tag in self.Tags:
+ cur_pos = self.f.tell()
+
+ logger.debug(
+ "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d",
+ TiffTags.lookup(tag).name,
+ tag,
+ TYPES.get(field_type, "unknown"),
+ field_type,
+ field_size,
+ count,
+ )
+
+ if is_local:
+ self._fixOffsets(count, field_size)
+ self.f.seek(cur_pos + fmt_size)
+ else:
+ self.f.seek(offset)
+ self._fixOffsets(count, field_size)
+ self.f.seek(cur_pos)
+
+ elif is_local:
+ # skip the locally stored value that is not an offset
+ self.f.seek(fmt_size, os.SEEK_CUR)
+
+ def _fixOffsets(self, count: int, field_size: int) -> None:
+ for i in range(count):
+ offset = self._read(field_size)
+ offset += self.offsetOfNewPage
+
+ new_field_size = 0
+ if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
+ # offset is now too large - we must convert long to long8
+ new_field_size = 8
+ elif field_size == 2 and offset >= 2**16:
+ # offset is now too large - we must convert short to long
+ new_field_size = 4
+ if new_field_size:
+ if count != 1:
+ msg = "not implemented"
+ raise RuntimeError(msg) # XXX TODO
+
+ # simple case - the offset is just one and therefore it is
+ # local (not referenced with another offset)
+ self._rewriteLast(offset, field_size, new_field_size)
+ # Move back past the new offset, past 'count', and before 'field_type'
+ rewind = -new_field_size - 4 - 2
+ self.f.seek(rewind, os.SEEK_CUR)
+ self.writeShort(new_field_size) # rewrite the type
+ self.f.seek(2 - rewind, os.SEEK_CUR)
+ else:
+ self._rewriteLast(offset, field_size)
+
+ def fixOffsets(
+ self, count: int, isShort: bool = False, isLong: bool = False
+ ) -> None:
+ if isShort:
+ field_size = 2
+ elif isLong:
+ field_size = 4
+ else:
+ field_size = 0
+ return self._fixOffsets(count, field_size)
+
+
+def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ append_images = list(im.encoderinfo.get("append_images", []))
+ if not hasattr(im, "n_frames") and not append_images:
+ return _save(im, fp, filename)
+
+ cur_idx = im.tell()
+ try:
+ with AppendingTiffWriter(fp) as tf:
+ for ims in [im] + append_images:
+ encoderinfo = ims._attach_default_encoderinfo(im)
+ if not hasattr(ims, "encoderconfig"):
+ ims.encoderconfig = ()
+ nfr = getattr(ims, "n_frames", 1)
+
+ for idx in range(nfr):
+ ims.seek(idx)
+ ims.load()
+ _save(ims, tf, filename)
+ tf.newFrame()
+ ims.encoderinfo = encoderinfo
+ finally:
+ im.seek(cur_idx)
+
+
+#
+# --------------------------------------------------------------------
+# Register
+
+Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
+Image.register_save(TiffImageFile.format, _save)
+Image.register_save_all(TiffImageFile.format, _save_all)
+
+Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])
+
+Image.register_mime(TiffImageFile.format, "image/tiff")
diff --git a/venv/lib/python3.11/site-packages/PIL/TiffTags.py b/venv/lib/python3.11/site-packages/PIL/TiffTags.py
new file mode 100644
index 000000000..613a3b7de
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/TiffTags.py
@@ -0,0 +1,566 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# TIFF tags
+#
+# This module provides clear-text names for various well-known
+# TIFF tags. the TIFF codec works just fine without it.
+#
+# Copyright (c) Secret Labs AB 1999.
+#
+# See the README file for information on usage and redistribution.
+#
+
+##
+# This module provides constants and clear-text names for various
+# well-known TIFF tags.
+##
+from __future__ import annotations
+
+from typing import NamedTuple
+
+
+class _TagInfo(NamedTuple):
+ value: int | None
+ name: str
+ type: int | None
+ length: int | None
+ enum: dict[str, int]
+
+
+class TagInfo(_TagInfo):
+ __slots__: list[str] = []
+
+ def __new__(
+ cls,
+ value: int | None = None,
+ name: str = "unknown",
+ type: int | None = None,
+ length: int | None = None,
+ enum: dict[str, int] | None = None,
+ ) -> TagInfo:
+ return super().__new__(cls, value, name, type, length, enum or {})
+
+ def cvt_enum(self, value: str) -> int | str:
+ # Using get will call hash(value), which can be expensive
+ # for some types (e.g. Fraction). Since self.enum is rarely
+ # used, it's usually better to test it first.
+ return self.enum.get(value, value) if self.enum else value
+
+
+def lookup(tag: int, group: int | None = None) -> TagInfo:
+ """
+ :param tag: Integer tag number
+ :param group: Which :py:data:`~PIL.TiffTags.TAGS_V2_GROUPS` to look in
+
+ .. versionadded:: 8.3.0
+
+ :returns: Taginfo namedtuple, From the ``TAGS_V2`` info if possible,
+ otherwise just populating the value and name from ``TAGS``.
+ If the tag is not recognized, "unknown" is returned for the name
+
+ """
+
+ if group is not None:
+ info = TAGS_V2_GROUPS[group].get(tag) if group in TAGS_V2_GROUPS else None
+ else:
+ info = TAGS_V2.get(tag)
+ return info or TagInfo(tag, TAGS.get(tag, "unknown"))
+
+
+##
+# Map tag numbers to tag info.
+#
+# id: (Name, Type, Length[, enum_values])
+#
+# The length here differs from the length in the tiff spec. For
+# numbers, the tiff spec is for the number of fields returned. We
+# agree here. For string-like types, the tiff spec uses the length of
+# field in bytes. In Pillow, we are using the number of expected
+# fields, in general 1 for string-like types.
+
+
+BYTE = 1
+ASCII = 2
+SHORT = 3
+LONG = 4
+RATIONAL = 5
+SIGNED_BYTE = 6
+UNDEFINED = 7
+SIGNED_SHORT = 8
+SIGNED_LONG = 9
+SIGNED_RATIONAL = 10
+FLOAT = 11
+DOUBLE = 12
+IFD = 13
+LONG8 = 16
+
+_tags_v2: dict[int, tuple[str, int, int] | tuple[str, int, int, dict[str, int]]] = {
+ 254: ("NewSubfileType", LONG, 1),
+ 255: ("SubfileType", SHORT, 1),
+ 256: ("ImageWidth", LONG, 1),
+ 257: ("ImageLength", LONG, 1),
+ 258: ("BitsPerSample", SHORT, 0),
+ 259: (
+ "Compression",
+ SHORT,
+ 1,
+ {
+ "Uncompressed": 1,
+ "CCITT 1d": 2,
+ "Group 3 Fax": 3,
+ "Group 4 Fax": 4,
+ "LZW": 5,
+ "JPEG": 6,
+ "PackBits": 32773,
+ },
+ ),
+ 262: (
+ "PhotometricInterpretation",
+ SHORT,
+ 1,
+ {
+ "WhiteIsZero": 0,
+ "BlackIsZero": 1,
+ "RGB": 2,
+ "RGB Palette": 3,
+ "Transparency Mask": 4,
+ "CMYK": 5,
+ "YCbCr": 6,
+ "CieLAB": 8,
+ "CFA": 32803, # TIFF/EP, Adobe DNG
+ "LinearRaw": 32892, # Adobe DNG
+ },
+ ),
+ 263: ("Threshholding", SHORT, 1),
+ 264: ("CellWidth", SHORT, 1),
+ 265: ("CellLength", SHORT, 1),
+ 266: ("FillOrder", SHORT, 1),
+ 269: ("DocumentName", ASCII, 1),
+ 270: ("ImageDescription", ASCII, 1),
+ 271: ("Make", ASCII, 1),
+ 272: ("Model", ASCII, 1),
+ 273: ("StripOffsets", LONG, 0),
+ 274: ("Orientation", SHORT, 1),
+ 277: ("SamplesPerPixel", SHORT, 1),
+ 278: ("RowsPerStrip", LONG, 1),
+ 279: ("StripByteCounts", LONG, 0),
+ 280: ("MinSampleValue", SHORT, 0),
+ 281: ("MaxSampleValue", SHORT, 0),
+ 282: ("XResolution", RATIONAL, 1),
+ 283: ("YResolution", RATIONAL, 1),
+ 284: ("PlanarConfiguration", SHORT, 1, {"Contiguous": 1, "Separate": 2}),
+ 285: ("PageName", ASCII, 1),
+ 286: ("XPosition", RATIONAL, 1),
+ 287: ("YPosition", RATIONAL, 1),
+ 288: ("FreeOffsets", LONG, 1),
+ 289: ("FreeByteCounts", LONG, 1),
+ 290: ("GrayResponseUnit", SHORT, 1),
+ 291: ("GrayResponseCurve", SHORT, 0),
+ 292: ("T4Options", LONG, 1),
+ 293: ("T6Options", LONG, 1),
+ 296: ("ResolutionUnit", SHORT, 1, {"none": 1, "inch": 2, "cm": 3}),
+ 297: ("PageNumber", SHORT, 2),
+ 301: ("TransferFunction", SHORT, 0),
+ 305: ("Software", ASCII, 1),
+ 306: ("DateTime", ASCII, 1),
+ 315: ("Artist", ASCII, 1),
+ 316: ("HostComputer", ASCII, 1),
+ 317: ("Predictor", SHORT, 1, {"none": 1, "Horizontal Differencing": 2}),
+ 318: ("WhitePoint", RATIONAL, 2),
+ 319: ("PrimaryChromaticities", RATIONAL, 6),
+ 320: ("ColorMap", SHORT, 0),
+ 321: ("HalftoneHints", SHORT, 2),
+ 322: ("TileWidth", LONG, 1),
+ 323: ("TileLength", LONG, 1),
+ 324: ("TileOffsets", LONG, 0),
+ 325: ("TileByteCounts", LONG, 0),
+ 330: ("SubIFDs", LONG, 0),
+ 332: ("InkSet", SHORT, 1),
+ 333: ("InkNames", ASCII, 1),
+ 334: ("NumberOfInks", SHORT, 1),
+ 336: ("DotRange", SHORT, 0),
+ 337: ("TargetPrinter", ASCII, 1),
+ 338: ("ExtraSamples", SHORT, 0),
+ 339: ("SampleFormat", SHORT, 0),
+ 340: ("SMinSampleValue", DOUBLE, 0),
+ 341: ("SMaxSampleValue", DOUBLE, 0),
+ 342: ("TransferRange", SHORT, 6),
+ 347: ("JPEGTables", UNDEFINED, 1),
+ # obsolete JPEG tags
+ 512: ("JPEGProc", SHORT, 1),
+ 513: ("JPEGInterchangeFormat", LONG, 1),
+ 514: ("JPEGInterchangeFormatLength", LONG, 1),
+ 515: ("JPEGRestartInterval", SHORT, 1),
+ 517: ("JPEGLosslessPredictors", SHORT, 0),
+ 518: ("JPEGPointTransforms", SHORT, 0),
+ 519: ("JPEGQTables", LONG, 0),
+ 520: ("JPEGDCTables", LONG, 0),
+ 521: ("JPEGACTables", LONG, 0),
+ 529: ("YCbCrCoefficients", RATIONAL, 3),
+ 530: ("YCbCrSubSampling", SHORT, 2),
+ 531: ("YCbCrPositioning", SHORT, 1),
+ 532: ("ReferenceBlackWhite", RATIONAL, 6),
+ 700: ("XMP", BYTE, 0),
+ # Four private SGI tags
+ 32995: ("Matteing", SHORT, 1),
+ 32996: ("DataType", SHORT, 0),
+ 32997: ("ImageDepth", LONG, 1),
+ 32998: ("TileDepth", LONG, 1),
+ 33432: ("Copyright", ASCII, 1),
+ 33723: ("IptcNaaInfo", UNDEFINED, 1),
+ 34377: ("PhotoshopInfo", BYTE, 0),
+ # FIXME add more tags here
+ 34665: ("ExifIFD", LONG, 1),
+ 34675: ("ICCProfile", UNDEFINED, 1),
+ 34853: ("GPSInfoIFD", LONG, 1),
+ 36864: ("ExifVersion", UNDEFINED, 1),
+ 37724: ("ImageSourceData", UNDEFINED, 1),
+ 40965: ("InteroperabilityIFD", LONG, 1),
+ 41730: ("CFAPattern", UNDEFINED, 1),
+ # MPInfo
+ 45056: ("MPFVersion", UNDEFINED, 1),
+ 45057: ("NumberOfImages", LONG, 1),
+ 45058: ("MPEntry", UNDEFINED, 1),
+ 45059: ("ImageUIDList", UNDEFINED, 0), # UNDONE, check
+ 45060: ("TotalFrames", LONG, 1),
+ 45313: ("MPIndividualNum", LONG, 1),
+ 45569: ("PanOrientation", LONG, 1),
+ 45570: ("PanOverlap_H", RATIONAL, 1),
+ 45571: ("PanOverlap_V", RATIONAL, 1),
+ 45572: ("BaseViewpointNum", LONG, 1),
+ 45573: ("ConvergenceAngle", SIGNED_RATIONAL, 1),
+ 45574: ("BaselineLength", RATIONAL, 1),
+ 45575: ("VerticalDivergence", SIGNED_RATIONAL, 1),
+ 45576: ("AxisDistance_X", SIGNED_RATIONAL, 1),
+ 45577: ("AxisDistance_Y", SIGNED_RATIONAL, 1),
+ 45578: ("AxisDistance_Z", SIGNED_RATIONAL, 1),
+ 45579: ("YawAngle", SIGNED_RATIONAL, 1),
+ 45580: ("PitchAngle", SIGNED_RATIONAL, 1),
+ 45581: ("RollAngle", SIGNED_RATIONAL, 1),
+ 40960: ("FlashPixVersion", UNDEFINED, 1),
+ 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}),
+ 50780: ("BestQualityScale", RATIONAL, 1),
+ 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one
+ 50839: ("ImageJMetaData", UNDEFINED, 1), # see Issue #2006
+}
+_tags_v2_groups = {
+ # ExifIFD
+ 34665: {
+ 36864: ("ExifVersion", UNDEFINED, 1),
+ 40960: ("FlashPixVersion", UNDEFINED, 1),
+ 40965: ("InteroperabilityIFD", LONG, 1),
+ 41730: ("CFAPattern", UNDEFINED, 1),
+ },
+ # GPSInfoIFD
+ 34853: {
+ 0: ("GPSVersionID", BYTE, 4),
+ 1: ("GPSLatitudeRef", ASCII, 2),
+ 2: ("GPSLatitude", RATIONAL, 3),
+ 3: ("GPSLongitudeRef", ASCII, 2),
+ 4: ("GPSLongitude", RATIONAL, 3),
+ 5: ("GPSAltitudeRef", BYTE, 1),
+ 6: ("GPSAltitude", RATIONAL, 1),
+ 7: ("GPSTimeStamp", RATIONAL, 3),
+ 8: ("GPSSatellites", ASCII, 0),
+ 9: ("GPSStatus", ASCII, 2),
+ 10: ("GPSMeasureMode", ASCII, 2),
+ 11: ("GPSDOP", RATIONAL, 1),
+ 12: ("GPSSpeedRef", ASCII, 2),
+ 13: ("GPSSpeed", RATIONAL, 1),
+ 14: ("GPSTrackRef", ASCII, 2),
+ 15: ("GPSTrack", RATIONAL, 1),
+ 16: ("GPSImgDirectionRef", ASCII, 2),
+ 17: ("GPSImgDirection", RATIONAL, 1),
+ 18: ("GPSMapDatum", ASCII, 0),
+ 19: ("GPSDestLatitudeRef", ASCII, 2),
+ 20: ("GPSDestLatitude", RATIONAL, 3),
+ 21: ("GPSDestLongitudeRef", ASCII, 2),
+ 22: ("GPSDestLongitude", RATIONAL, 3),
+ 23: ("GPSDestBearingRef", ASCII, 2),
+ 24: ("GPSDestBearing", RATIONAL, 1),
+ 25: ("GPSDestDistanceRef", ASCII, 2),
+ 26: ("GPSDestDistance", RATIONAL, 1),
+ 27: ("GPSProcessingMethod", UNDEFINED, 0),
+ 28: ("GPSAreaInformation", UNDEFINED, 0),
+ 29: ("GPSDateStamp", ASCII, 11),
+ 30: ("GPSDifferential", SHORT, 1),
+ },
+ # InteroperabilityIFD
+ 40965: {1: ("InteropIndex", ASCII, 1), 2: ("InteropVersion", UNDEFINED, 1)},
+}
+
+# Legacy Tags structure
+# these tags aren't included above, but were in the previous versions
+TAGS: dict[int | tuple[int, int], str] = {
+ 347: "JPEGTables",
+ 700: "XMP",
+ # Additional Exif Info
+ 32932: "Wang Annotation",
+ 33434: "ExposureTime",
+ 33437: "FNumber",
+ 33445: "MD FileTag",
+ 33446: "MD ScalePixel",
+ 33447: "MD ColorTable",
+ 33448: "MD LabName",
+ 33449: "MD SampleInfo",
+ 33450: "MD PrepDate",
+ 33451: "MD PrepTime",
+ 33452: "MD FileUnits",
+ 33550: "ModelPixelScaleTag",
+ 33723: "IptcNaaInfo",
+ 33918: "INGR Packet Data Tag",
+ 33919: "INGR Flag Registers",
+ 33920: "IrasB Transformation Matrix",
+ 33922: "ModelTiepointTag",
+ 34264: "ModelTransformationTag",
+ 34377: "PhotoshopInfo",
+ 34735: "GeoKeyDirectoryTag",
+ 34736: "GeoDoubleParamsTag",
+ 34737: "GeoAsciiParamsTag",
+ 34850: "ExposureProgram",
+ 34852: "SpectralSensitivity",
+ 34855: "ISOSpeedRatings",
+ 34856: "OECF",
+ 34864: "SensitivityType",
+ 34865: "StandardOutputSensitivity",
+ 34866: "RecommendedExposureIndex",
+ 34867: "ISOSpeed",
+ 34868: "ISOSpeedLatitudeyyy",
+ 34869: "ISOSpeedLatitudezzz",
+ 34908: "HylaFAX FaxRecvParams",
+ 34909: "HylaFAX FaxSubAddress",
+ 34910: "HylaFAX FaxRecvTime",
+ 36864: "ExifVersion",
+ 36867: "DateTimeOriginal",
+ 36868: "DateTimeDigitized",
+ 37121: "ComponentsConfiguration",
+ 37122: "CompressedBitsPerPixel",
+ 37724: "ImageSourceData",
+ 37377: "ShutterSpeedValue",
+ 37378: "ApertureValue",
+ 37379: "BrightnessValue",
+ 37380: "ExposureBiasValue",
+ 37381: "MaxApertureValue",
+ 37382: "SubjectDistance",
+ 37383: "MeteringMode",
+ 37384: "LightSource",
+ 37385: "Flash",
+ 37386: "FocalLength",
+ 37396: "SubjectArea",
+ 37500: "MakerNote",
+ 37510: "UserComment",
+ 37520: "SubSec",
+ 37521: "SubSecTimeOriginal",
+ 37522: "SubsecTimeDigitized",
+ 40960: "FlashPixVersion",
+ 40961: "ColorSpace",
+ 40962: "PixelXDimension",
+ 40963: "PixelYDimension",
+ 40964: "RelatedSoundFile",
+ 40965: "InteroperabilityIFD",
+ 41483: "FlashEnergy",
+ 41484: "SpatialFrequencyResponse",
+ 41486: "FocalPlaneXResolution",
+ 41487: "FocalPlaneYResolution",
+ 41488: "FocalPlaneResolutionUnit",
+ 41492: "SubjectLocation",
+ 41493: "ExposureIndex",
+ 41495: "SensingMethod",
+ 41728: "FileSource",
+ 41729: "SceneType",
+ 41730: "CFAPattern",
+ 41985: "CustomRendered",
+ 41986: "ExposureMode",
+ 41987: "WhiteBalance",
+ 41988: "DigitalZoomRatio",
+ 41989: "FocalLengthIn35mmFilm",
+ 41990: "SceneCaptureType",
+ 41991: "GainControl",
+ 41992: "Contrast",
+ 41993: "Saturation",
+ 41994: "Sharpness",
+ 41995: "DeviceSettingDescription",
+ 41996: "SubjectDistanceRange",
+ 42016: "ImageUniqueID",
+ 42032: "CameraOwnerName",
+ 42033: "BodySerialNumber",
+ 42034: "LensSpecification",
+ 42035: "LensMake",
+ 42036: "LensModel",
+ 42037: "LensSerialNumber",
+ 42112: "GDAL_METADATA",
+ 42113: "GDAL_NODATA",
+ 42240: "Gamma",
+ 50215: "Oce Scanjob Description",
+ 50216: "Oce Application Selector",
+ 50217: "Oce Identification Number",
+ 50218: "Oce ImageLogic Characteristics",
+ # Adobe DNG
+ 50706: "DNGVersion",
+ 50707: "DNGBackwardVersion",
+ 50708: "UniqueCameraModel",
+ 50709: "LocalizedCameraModel",
+ 50710: "CFAPlaneColor",
+ 50711: "CFALayout",
+ 50712: "LinearizationTable",
+ 50713: "BlackLevelRepeatDim",
+ 50714: "BlackLevel",
+ 50715: "BlackLevelDeltaH",
+ 50716: "BlackLevelDeltaV",
+ 50717: "WhiteLevel",
+ 50718: "DefaultScale",
+ 50719: "DefaultCropOrigin",
+ 50720: "DefaultCropSize",
+ 50721: "ColorMatrix1",
+ 50722: "ColorMatrix2",
+ 50723: "CameraCalibration1",
+ 50724: "CameraCalibration2",
+ 50725: "ReductionMatrix1",
+ 50726: "ReductionMatrix2",
+ 50727: "AnalogBalance",
+ 50728: "AsShotNeutral",
+ 50729: "AsShotWhiteXY",
+ 50730: "BaselineExposure",
+ 50731: "BaselineNoise",
+ 50732: "BaselineSharpness",
+ 50733: "BayerGreenSplit",
+ 50734: "LinearResponseLimit",
+ 50735: "CameraSerialNumber",
+ 50736: "LensInfo",
+ 50737: "ChromaBlurRadius",
+ 50738: "AntiAliasStrength",
+ 50740: "DNGPrivateData",
+ 50778: "CalibrationIlluminant1",
+ 50779: "CalibrationIlluminant2",
+ 50784: "Alias Layer Metadata",
+}
+
+TAGS_V2: dict[int, TagInfo] = {}
+TAGS_V2_GROUPS: dict[int, dict[int, TagInfo]] = {}
+
+
+def _populate() -> None:
+ for k, v in _tags_v2.items():
+ # Populate legacy structure.
+ TAGS[k] = v[0]
+ if len(v) == 4:
+ for sk, sv in v[3].items():
+ TAGS[(k, sv)] = sk
+
+ TAGS_V2[k] = TagInfo(k, *v)
+
+ for group, tags in _tags_v2_groups.items():
+ TAGS_V2_GROUPS[group] = {k: TagInfo(k, *v) for k, v in tags.items()}
+
+
+_populate()
+##
+# Map type numbers to type names -- defined in ImageFileDirectory.
+
+TYPES: dict[int, str] = {}
+
+#
+# These tags are handled by default in libtiff, without
+# adding to the custom dictionary. From tif_dir.c, searching for
+# case TIFFTAG in the _TIFFVSetField function:
+# Line: item.
+# 148: case TIFFTAG_SUBFILETYPE:
+# 151: case TIFFTAG_IMAGEWIDTH:
+# 154: case TIFFTAG_IMAGELENGTH:
+# 157: case TIFFTAG_BITSPERSAMPLE:
+# 181: case TIFFTAG_COMPRESSION:
+# 202: case TIFFTAG_PHOTOMETRIC:
+# 205: case TIFFTAG_THRESHHOLDING:
+# 208: case TIFFTAG_FILLORDER:
+# 214: case TIFFTAG_ORIENTATION:
+# 221: case TIFFTAG_SAMPLESPERPIXEL:
+# 228: case TIFFTAG_ROWSPERSTRIP:
+# 238: case TIFFTAG_MINSAMPLEVALUE:
+# 241: case TIFFTAG_MAXSAMPLEVALUE:
+# 244: case TIFFTAG_SMINSAMPLEVALUE:
+# 247: case TIFFTAG_SMAXSAMPLEVALUE:
+# 250: case TIFFTAG_XRESOLUTION:
+# 256: case TIFFTAG_YRESOLUTION:
+# 262: case TIFFTAG_PLANARCONFIG:
+# 268: case TIFFTAG_XPOSITION:
+# 271: case TIFFTAG_YPOSITION:
+# 274: case TIFFTAG_RESOLUTIONUNIT:
+# 280: case TIFFTAG_PAGENUMBER:
+# 284: case TIFFTAG_HALFTONEHINTS:
+# 288: case TIFFTAG_COLORMAP:
+# 294: case TIFFTAG_EXTRASAMPLES:
+# 298: case TIFFTAG_MATTEING:
+# 305: case TIFFTAG_TILEWIDTH:
+# 316: case TIFFTAG_TILELENGTH:
+# 327: case TIFFTAG_TILEDEPTH:
+# 333: case TIFFTAG_DATATYPE:
+# 344: case TIFFTAG_SAMPLEFORMAT:
+# 361: case TIFFTAG_IMAGEDEPTH:
+# 364: case TIFFTAG_SUBIFD:
+# 376: case TIFFTAG_YCBCRPOSITIONING:
+# 379: case TIFFTAG_YCBCRSUBSAMPLING:
+# 383: case TIFFTAG_TRANSFERFUNCTION:
+# 389: case TIFFTAG_REFERENCEBLACKWHITE:
+# 393: case TIFFTAG_INKNAMES:
+
+# Following pseudo-tags are also handled by default in libtiff:
+# TIFFTAG_JPEGQUALITY 65537
+
+# some of these are not in our TAGS_V2 dict and were included from tiff.h
+
+# This list also exists in encode.c
+LIBTIFF_CORE = {
+ 255,
+ 256,
+ 257,
+ 258,
+ 259,
+ 262,
+ 263,
+ 266,
+ 274,
+ 277,
+ 278,
+ 280,
+ 281,
+ 340,
+ 341,
+ 282,
+ 283,
+ 284,
+ 286,
+ 287,
+ 296,
+ 297,
+ 321,
+ 320,
+ 338,
+ 32995,
+ 322,
+ 323,
+ 32998,
+ 32996,
+ 339,
+ 32997,
+ 330,
+ 531,
+ 530,
+ 301,
+ 532,
+ 333,
+ # as above
+ 269, # this has been in our tests forever, and works
+ 65537,
+}
+
+LIBTIFF_CORE.remove(255) # We don't have support for subfiletypes
+LIBTIFF_CORE.remove(322) # We don't have support for writing tiled images with libtiff
+LIBTIFF_CORE.remove(323) # Tiled images
+
+# Note to advanced users: There may be combinations of these
+# parameters and values that when added properly, will work and
+# produce valid tiff images that may work in your application.
+# It is safe to add and remove tags from this set from Pillow's point
+# of view so long as you test against libtiff.
diff --git a/venv/lib/python3.11/site-packages/PIL/WalImageFile.py b/venv/lib/python3.11/site-packages/PIL/WalImageFile.py
new file mode 100644
index 000000000..07bbf7471
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/WalImageFile.py
@@ -0,0 +1,129 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# WAL file handling
+#
+# History:
+# 2003-04-23 fl created
+#
+# Copyright (c) 2003 by Fredrik Lundh.
+#
+# See the README file for information on usage and redistribution.
+#
+
+"""
+This reader is based on the specification available from:
+https://www.flipcode.com/archives/Quake_2_BSP_File_Format.shtml
+and has been tested with a few sample files found using google.
+
+.. note::
+ This format cannot be automatically recognized, so the reader
+ is not registered for use with :py:func:`PIL.Image.open()`.
+ To open a WAL file, use the :py:func:`PIL.WalImageFile.open()` function instead.
+"""
+
+from __future__ import annotations
+
+from typing import IO
+
+from . import Image, ImageFile
+from ._binary import i32le as i32
+from ._typing import StrOrBytesPath
+
+
+class WalImageFile(ImageFile.ImageFile):
+ format = "WAL"
+ format_description = "Quake2 Texture"
+
+ def _open(self) -> None:
+ self._mode = "P"
+
+ # read header fields
+ assert self.fp is not None
+ header = self.fp.read(32 + 24 + 32 + 12)
+ self._size = i32(header, 32), i32(header, 36)
+ Image._decompression_bomb_check(self.size)
+
+ # load pixel data
+ offset = i32(header, 40)
+ self.fp.seek(offset)
+
+ # strings are null-terminated
+ self.info["name"] = header[:32].split(b"\0", 1)[0]
+ if next_name := header[56 : 56 + 32].split(b"\0", 1)[0]:
+ self.info["next_name"] = next_name
+
+ def load(self) -> Image.core.PixelAccess | None:
+ if self._im is None:
+ assert self.fp is not None
+ self.im = Image.core.new(self.mode, self.size)
+ self.frombytes(self.fp.read(self.size[0] * self.size[1]))
+ self.putpalette(quake2palette)
+ return Image.Image.load(self)
+
+
+def open(filename: StrOrBytesPath | IO[bytes]) -> WalImageFile:
+ """
+ Load texture from a Quake2 WAL texture file.
+
+ By default, a Quake2 standard palette is attached to the texture.
+ To override the palette, use the :py:func:`PIL.Image.Image.putpalette()` method.
+
+ :param filename: WAL file name, or an opened file handle.
+ :returns: An image instance.
+ """
+ return WalImageFile(filename)
+
+
+quake2palette = (
+ # default palette taken from piffo 0.93 by Hans Häggström
+ b"\x01\x01\x01\x0b\x0b\x0b\x12\x12\x12\x17\x17\x17\x1b\x1b\x1b\x1e"
+ b"\x1e\x1e\x22\x22\x22\x26\x26\x26\x29\x29\x29\x2c\x2c\x2c\x2f\x2f"
+ b"\x2f\x32\x32\x32\x35\x35\x35\x37\x37\x37\x3a\x3a\x3a\x3c\x3c\x3c"
+ b"\x24\x1e\x13\x22\x1c\x12\x20\x1b\x12\x1f\x1a\x10\x1d\x19\x10\x1b"
+ b"\x17\x0f\x1a\x16\x0f\x18\x14\x0d\x17\x13\x0d\x16\x12\x0d\x14\x10"
+ b"\x0b\x13\x0f\x0b\x10\x0d\x0a\x0f\x0b\x0a\x0d\x0b\x07\x0b\x0a\x07"
+ b"\x23\x23\x26\x22\x22\x25\x22\x20\x23\x21\x1f\x22\x20\x1e\x20\x1f"
+ b"\x1d\x1e\x1d\x1b\x1c\x1b\x1a\x1a\x1a\x19\x19\x18\x17\x17\x17\x16"
+ b"\x16\x14\x14\x14\x13\x13\x13\x10\x10\x10\x0f\x0f\x0f\x0d\x0d\x0d"
+ b"\x2d\x28\x20\x29\x24\x1c\x27\x22\x1a\x25\x1f\x17\x38\x2e\x1e\x31"
+ b"\x29\x1a\x2c\x25\x17\x26\x20\x14\x3c\x30\x14\x37\x2c\x13\x33\x28"
+ b"\x12\x2d\x24\x10\x28\x1f\x0f\x22\x1a\x0b\x1b\x14\x0a\x13\x0f\x07"
+ b"\x31\x1a\x16\x30\x17\x13\x2e\x16\x10\x2c\x14\x0d\x2a\x12\x0b\x27"
+ b"\x0f\x0a\x25\x0f\x07\x21\x0d\x01\x1e\x0b\x01\x1c\x0b\x01\x1a\x0b"
+ b"\x01\x18\x0a\x01\x16\x0a\x01\x13\x0a\x01\x10\x07\x01\x0d\x07\x01"
+ b"\x29\x23\x1e\x27\x21\x1c\x26\x20\x1b\x25\x1f\x1a\x23\x1d\x19\x21"
+ b"\x1c\x18\x20\x1b\x17\x1e\x19\x16\x1c\x18\x14\x1b\x17\x13\x19\x14"
+ b"\x10\x17\x13\x0f\x14\x10\x0d\x12\x0f\x0b\x0f\x0b\x0a\x0b\x0a\x07"
+ b"\x26\x1a\x0f\x23\x19\x0f\x20\x17\x0f\x1c\x16\x0f\x19\x13\x0d\x14"
+ b"\x10\x0b\x10\x0d\x0a\x0b\x0a\x07\x33\x22\x1f\x35\x29\x26\x37\x2f"
+ b"\x2d\x39\x35\x34\x37\x39\x3a\x33\x37\x39\x30\x34\x36\x2b\x31\x34"
+ b"\x27\x2e\x31\x22\x2b\x2f\x1d\x28\x2c\x17\x25\x2a\x0f\x20\x26\x0d"
+ b"\x1e\x25\x0b\x1c\x22\x0a\x1b\x20\x07\x19\x1e\x07\x17\x1b\x07\x14"
+ b"\x18\x01\x12\x16\x01\x0f\x12\x01\x0b\x0d\x01\x07\x0a\x01\x01\x01"
+ b"\x2c\x21\x21\x2a\x1f\x1f\x29\x1d\x1d\x27\x1c\x1c\x26\x1a\x1a\x24"
+ b"\x18\x18\x22\x17\x17\x21\x16\x16\x1e\x13\x13\x1b\x12\x12\x18\x10"
+ b"\x10\x16\x0d\x0d\x12\x0b\x0b\x0d\x0a\x0a\x0a\x07\x07\x01\x01\x01"
+ b"\x2e\x30\x29\x2d\x2e\x27\x2b\x2c\x26\x2a\x2a\x24\x28\x29\x23\x27"
+ b"\x27\x21\x26\x26\x1f\x24\x24\x1d\x22\x22\x1c\x1f\x1f\x1a\x1c\x1c"
+ b"\x18\x19\x19\x16\x17\x17\x13\x13\x13\x10\x0f\x0f\x0d\x0b\x0b\x0a"
+ b"\x30\x1e\x1b\x2d\x1c\x19\x2c\x1a\x17\x2a\x19\x14\x28\x17\x13\x26"
+ b"\x16\x10\x24\x13\x0f\x21\x12\x0d\x1f\x10\x0b\x1c\x0f\x0a\x19\x0d"
+ b"\x0a\x16\x0b\x07\x12\x0a\x07\x0f\x07\x01\x0a\x01\x01\x01\x01\x01"
+ b"\x28\x29\x38\x26\x27\x36\x25\x26\x34\x24\x24\x31\x22\x22\x2f\x20"
+ b"\x21\x2d\x1e\x1f\x2a\x1d\x1d\x27\x1b\x1b\x25\x19\x19\x21\x17\x17"
+ b"\x1e\x14\x14\x1b\x13\x12\x17\x10\x0f\x13\x0d\x0b\x0f\x0a\x07\x07"
+ b"\x2f\x32\x29\x2d\x30\x26\x2b\x2e\x24\x29\x2c\x21\x27\x2a\x1e\x25"
+ b"\x28\x1c\x23\x26\x1a\x21\x25\x18\x1e\x22\x14\x1b\x1f\x10\x19\x1c"
+ b"\x0d\x17\x1a\x0a\x13\x17\x07\x10\x13\x01\x0d\x0f\x01\x0a\x0b\x01"
+ b"\x01\x3f\x01\x13\x3c\x0b\x1b\x39\x10\x20\x35\x14\x23\x31\x17\x23"
+ b"\x2d\x18\x23\x29\x18\x3f\x3f\x3f\x3f\x3f\x39\x3f\x3f\x31\x3f\x3f"
+ b"\x2a\x3f\x3f\x20\x3f\x3f\x14\x3f\x3c\x12\x3f\x39\x0f\x3f\x35\x0b"
+ b"\x3f\x32\x07\x3f\x2d\x01\x3d\x2a\x01\x3b\x26\x01\x39\x21\x01\x37"
+ b"\x1d\x01\x34\x1a\x01\x32\x16\x01\x2f\x12\x01\x2d\x0f\x01\x2a\x0b"
+ b"\x01\x27\x07\x01\x23\x01\x01\x1d\x01\x01\x17\x01\x01\x10\x01\x01"
+ b"\x3d\x01\x01\x19\x19\x3f\x3f\x01\x01\x01\x01\x3f\x16\x16\x13\x10"
+ b"\x10\x0f\x0d\x0d\x0b\x3c\x2e\x2a\x36\x27\x20\x30\x21\x18\x29\x1b"
+ b"\x10\x3c\x39\x37\x37\x32\x2f\x31\x2c\x28\x2b\x26\x21\x30\x22\x20"
+)
diff --git a/venv/lib/python3.11/site-packages/PIL/WebPImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/WebPImagePlugin.py
new file mode 100644
index 000000000..63a481691
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/WebPImagePlugin.py
@@ -0,0 +1,322 @@
+from __future__ import annotations
+
+from io import BytesIO
+
+from . import Image, ImageFile
+
+try:
+ from . import _webp
+
+ SUPPORTED = True
+except ImportError:
+ SUPPORTED = False
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import IO, Any
+
+_VP8_MODES_BY_IDENTIFIER = {
+ b"VP8 ": "RGB",
+ b"VP8X": "RGBA",
+ b"VP8L": "RGBA", # lossless
+}
+
+
+def _accept(prefix: bytes) -> bool | str:
+ is_riff_file_format = prefix.startswith(b"RIFF")
+ is_webp_file = prefix[8:12] == b"WEBP"
+ is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER
+
+ if is_riff_file_format and is_webp_file and is_valid_vp8_mode:
+ if not SUPPORTED:
+ return (
+ "image file could not be identified because WEBP support not installed"
+ )
+ return True
+ return False
+
+
+class WebPImageFile(ImageFile.ImageFile):
+ format = "WEBP"
+ format_description = "WebP image"
+ __loaded = 0
+ __logical_frame = 0
+
+ def _open(self) -> None:
+ assert self.fp is not None
+ s = self.fp.read()
+ if not _accept(s):
+ msg = "not a WEBP file"
+ raise SyntaxError(msg)
+
+ # Use the newer AnimDecoder API to parse the (possibly) animated file,
+ # and access muxed chunks like ICC/EXIF/XMP.
+ self._decoder = _webp.WebPAnimDecoder(s)
+
+ # Get info from decoder
+ self._size, self.info["loop"], bgcolor, self.n_frames, self.rawmode = (
+ self._decoder.get_info()
+ )
+ self.info["background"] = (
+ (bgcolor >> 16) & 0xFF, # R
+ (bgcolor >> 8) & 0xFF, # G
+ bgcolor & 0xFF, # B
+ (bgcolor >> 24) & 0xFF, # A
+ )
+ self.is_animated = self.n_frames > 1
+ self._mode = "RGB" if self.rawmode == "RGBX" else self.rawmode
+
+ # Attempt to read ICC / EXIF / XMP chunks from file
+ for key, chunk_name in {
+ "icc_profile": "ICCP",
+ "exif": "EXIF",
+ "xmp": "XMP ",
+ }.items():
+ if value := self._decoder.get_chunk(chunk_name):
+ self.info[key] = value
+
+ # Initialize seek state
+ self._reset(reset=False)
+
+ def _getexif(self) -> dict[int, Any] | None:
+ if "exif" not in self.info:
+ return None
+ return self.getexif()._get_merged_dict()
+
+ def seek(self, frame: int) -> None:
+ if not self._seek_check(frame):
+ return
+
+ # Set logical frame to requested position
+ self.__logical_frame = frame
+
+ def _reset(self, reset: bool = True) -> None:
+ if reset:
+ self._decoder.reset()
+ self.__physical_frame = 0
+ self.__loaded = -1
+ self.__timestamp = 0
+
+ def _get_next(self) -> tuple[bytes, int, int]:
+ # Get next frame
+ ret = self._decoder.get_next()
+ self.__physical_frame += 1
+
+ # Check if an error occurred
+ if ret is None:
+ self._reset() # Reset just to be safe
+ self.seek(0)
+ msg = "failed to decode next frame in WebP file"
+ raise EOFError(msg)
+
+ # Compute duration
+ data, timestamp = ret
+ duration = timestamp - self.__timestamp
+ self.__timestamp = timestamp
+
+ # libwebp gives frame end, adjust to start of frame
+ timestamp -= duration
+ return data, timestamp, duration
+
+ def _seek(self, frame: int) -> None:
+ if self.__physical_frame == frame:
+ return # Nothing to do
+ if frame < self.__physical_frame:
+ self._reset() # Rewind to beginning
+ while self.__physical_frame < frame:
+ self._get_next() # Advance to the requested frame
+
+ def load(self) -> Image.core.PixelAccess | None:
+ if self.__loaded != self.__logical_frame:
+ self._seek(self.__logical_frame)
+
+ # We need to load the image data for this frame
+ data, self.info["timestamp"], self.info["duration"] = self._get_next()
+ self.__loaded = self.__logical_frame
+
+ # Set tile
+ if self.fp and self._exclusive_fp:
+ self.fp.close()
+ self.fp = BytesIO(data)
+ self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)]
+
+ return super().load()
+
+ def load_seek(self, pos: int) -> None:
+ pass
+
+ def tell(self) -> int:
+ return self.__logical_frame
+
+
+def _convert_frame(im: Image.Image) -> Image.Image:
+ # Make sure image mode is supported
+ if im.mode not in ("RGBX", "RGBA", "RGB"):
+ im = im.convert("RGBA" if im.has_transparency_data else "RGB")
+ return im
+
+
+def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ encoderinfo = im.encoderinfo.copy()
+ append_images = list(encoderinfo.get("append_images", []))
+
+ # If total frame count is 1, then save using the legacy API, which
+ # will preserve non-alpha modes
+ total = 0
+ for ims in [im] + append_images:
+ total += getattr(ims, "n_frames", 1)
+ if total == 1:
+ _save(im, fp, filename)
+ return
+
+ background: int | tuple[int, ...] = (0, 0, 0, 0)
+ if "background" in encoderinfo:
+ background = encoderinfo["background"]
+ elif "background" in im.info:
+ background = im.info["background"]
+ if isinstance(background, int):
+ # GifImagePlugin stores a global color table index in
+ # info["background"]. So it must be converted to an RGBA value
+ palette = im.getpalette()
+ if palette:
+ r, g, b = palette[background * 3 : (background + 1) * 3]
+ background = (r, g, b, 255)
+ else:
+ background = (background, background, background, 255)
+
+ duration = im.encoderinfo.get("duration", im.info.get("duration", 0))
+ loop = im.encoderinfo.get("loop", 0)
+ minimize_size = im.encoderinfo.get("minimize_size", False)
+ kmin = im.encoderinfo.get("kmin", None)
+ kmax = im.encoderinfo.get("kmax", None)
+ allow_mixed = im.encoderinfo.get("allow_mixed", False)
+ verbose = False
+ lossless = im.encoderinfo.get("lossless", False)
+ quality = im.encoderinfo.get("quality", 80)
+ alpha_quality = im.encoderinfo.get("alpha_quality", 100)
+ method = im.encoderinfo.get("method", 0)
+ icc_profile = im.encoderinfo.get("icc_profile") or ""
+ exif = im.encoderinfo.get("exif", "")
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes()
+ xmp = im.encoderinfo.get("xmp", "")
+ if allow_mixed:
+ lossless = False
+
+ # Sensible keyframe defaults are from gif2webp.c script
+ if kmin is None:
+ kmin = 9 if lossless else 3
+ if kmax is None:
+ kmax = 17 if lossless else 5
+
+ # Validate background color
+ if (
+ not isinstance(background, (list, tuple))
+ or len(background) != 4
+ or not all(0 <= v < 256 for v in background)
+ ):
+ msg = f"Background color is not an RGBA tuple clamped to (0-255): {background}"
+ raise OSError(msg)
+
+ # Convert to packed uint
+ bg_r, bg_g, bg_b, bg_a = background
+ background = (bg_a << 24) | (bg_r << 16) | (bg_g << 8) | (bg_b << 0)
+
+ # Setup the WebP animation encoder
+ enc = _webp.WebPAnimEncoder(
+ im.size,
+ background,
+ loop,
+ minimize_size,
+ kmin,
+ kmax,
+ allow_mixed,
+ verbose,
+ )
+
+ # Add each frame
+ frame_idx = 0
+ timestamp = 0
+ cur_idx = im.tell()
+ try:
+ for ims in [im] + append_images:
+ # Get number of frames in this image
+ nfr = getattr(ims, "n_frames", 1)
+
+ for idx in range(nfr):
+ ims.seek(idx)
+
+ frame = _convert_frame(ims)
+
+ # Append the frame to the animation encoder
+ enc.add(
+ frame.getim(),
+ round(timestamp),
+ lossless,
+ quality,
+ alpha_quality,
+ method,
+ )
+
+ # Update timestamp and frame index
+ if isinstance(duration, (list, tuple)):
+ timestamp += duration[frame_idx]
+ else:
+ timestamp += duration
+ frame_idx += 1
+
+ finally:
+ im.seek(cur_idx)
+
+ # Force encoder to flush frames
+ enc.add(None, round(timestamp), lossless, quality, alpha_quality, 0)
+
+ # Get the final output from the encoder
+ data = enc.assemble(icc_profile, exif, xmp)
+ if data is None:
+ msg = "cannot write file as WebP (encoder returned None)"
+ raise OSError(msg)
+
+ fp.write(data)
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ lossless = im.encoderinfo.get("lossless", False)
+ quality = im.encoderinfo.get("quality", 80)
+ alpha_quality = im.encoderinfo.get("alpha_quality", 100)
+ icc_profile = im.encoderinfo.get("icc_profile") or ""
+ exif = im.encoderinfo.get("exif", b"")
+ if isinstance(exif, Image.Exif):
+ exif = exif.tobytes()
+ if exif.startswith(b"Exif\x00\x00"):
+ exif = exif[6:]
+ xmp = im.encoderinfo.get("xmp", "")
+ method = im.encoderinfo.get("method", 4)
+ exact = 1 if im.encoderinfo.get("exact") else 0
+
+ im = _convert_frame(im)
+
+ data = _webp.WebPEncode(
+ im.getim(),
+ lossless,
+ float(quality),
+ float(alpha_quality),
+ icc_profile,
+ method,
+ exact,
+ exif,
+ xmp,
+ )
+ if data is None:
+ msg = "cannot write file as WebP (encoder returned None)"
+ raise OSError(msg)
+
+ fp.write(data)
+
+
+Image.register_open(WebPImageFile.format, WebPImageFile, _accept)
+if SUPPORTED:
+ Image.register_save(WebPImageFile.format, _save)
+ Image.register_save_all(WebPImageFile.format, _save_all)
+ Image.register_extension(WebPImageFile.format, ".webp")
+ Image.register_mime(WebPImageFile.format, "image/webp")
diff --git a/venv/lib/python3.11/site-packages/PIL/WmfImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/WmfImagePlugin.py
new file mode 100644
index 000000000..f5e244782
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/WmfImagePlugin.py
@@ -0,0 +1,183 @@
+#
+# The Python Imaging Library
+# $Id$
+#
+# WMF stub codec
+#
+# history:
+# 1996-12-14 fl Created
+# 2004-02-22 fl Turned into a stub driver
+# 2004-02-23 fl Added EMF support
+#
+# Copyright (c) Secret Labs AB 1997-2004. All rights reserved.
+# Copyright (c) Fredrik Lundh 1996.
+#
+# See the README file for information on usage and redistribution.
+#
+# WMF/EMF reference documentation:
+# https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-WMF/[MS-WMF].pdf
+# http://wvware.sourceforge.net/caolan/index.html
+# http://wvware.sourceforge.net/caolan/ora-wmf.html
+from __future__ import annotations
+
+from typing import IO
+
+from . import Image, ImageFile
+from ._binary import i16le as word
+from ._binary import si16le as short
+from ._binary import si32le as _long
+
+_handler = None
+
+
+def register_handler(handler: ImageFile.StubHandler | None) -> None:
+ """
+ Install application-specific WMF image handler.
+
+ :param handler: Handler object.
+ """
+ global _handler
+ _handler = handler
+
+
+if hasattr(Image.core, "drawwmf"):
+ # install default handler (windows only)
+
+ class WmfHandler(ImageFile.StubHandler):
+ def open(self, im: ImageFile.StubImageFile) -> None:
+ self.bbox = im.info["wmf_bbox"]
+
+ def load(self, im: ImageFile.StubImageFile) -> Image.Image:
+ assert im.fp is not None
+ im.fp.seek(0) # rewind
+ return Image.frombytes(
+ "RGB",
+ im.size,
+ Image.core.drawwmf(im.fp.read(), im.size, self.bbox),
+ "raw",
+ "BGR",
+ (im.size[0] * 3 + 3) & -4,
+ -1,
+ )
+
+ register_handler(WmfHandler())
+
+#
+# --------------------------------------------------------------------
+# Read WMF file
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith((b"\xd7\xcd\xc6\x9a\x00\x00", b"\x01\x00\x00\x00"))
+
+
+##
+# Image plugin for Windows metafiles.
+
+
+class WmfStubImageFile(ImageFile.StubImageFile):
+ format = "WMF"
+ format_description = "Windows Metafile"
+
+ def _open(self) -> None:
+ # check placeable header
+ assert self.fp is not None
+ s = self.fp.read(44)
+
+ if s.startswith(b"\xd7\xcd\xc6\x9a\x00\x00"):
+ # placeable windows metafile
+
+ # get units per inch
+ inch = word(s, 14)
+ if inch == 0:
+ msg = "Invalid inch"
+ raise ValueError(msg)
+ self._inch: tuple[float, float] = inch, inch
+
+ # get bounding box
+ x0 = short(s, 6)
+ y0 = short(s, 8)
+ x1 = short(s, 10)
+ y1 = short(s, 12)
+
+ # normalize size to 72 dots per inch
+ self.info["dpi"] = 72
+ size = (
+ (x1 - x0) * self.info["dpi"] // inch,
+ (y1 - y0) * self.info["dpi"] // inch,
+ )
+
+ self.info["wmf_bbox"] = x0, y0, x1, y1
+
+ # sanity check (standard metafile header)
+ if s[22:26] != b"\x01\x00\t\x00":
+ msg = "Unsupported WMF file format"
+ raise SyntaxError(msg)
+
+ elif s.startswith(b"\x01\x00\x00\x00") and s[40:44] == b" EMF":
+ # enhanced metafile
+
+ # get bounding box
+ x0 = _long(s, 8)
+ y0 = _long(s, 12)
+ x1 = _long(s, 16)
+ y1 = _long(s, 20)
+
+ # get frame (in 0.01 millimeter units)
+ frame = _long(s, 24), _long(s, 28), _long(s, 32), _long(s, 36)
+
+ size = x1 - x0, y1 - y0
+
+ # calculate dots per inch from bbox and frame
+ xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0])
+ ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1])
+
+ self.info["wmf_bbox"] = x0, y0, x1, y1
+
+ if xdpi == ydpi:
+ self.info["dpi"] = xdpi
+ else:
+ self.info["dpi"] = xdpi, ydpi
+ self._inch = xdpi, ydpi
+
+ else:
+ msg = "Unsupported file format"
+ raise SyntaxError(msg)
+
+ self._mode = "RGB"
+ self._size = size
+
+ def _load(self) -> ImageFile.StubHandler | None:
+ return _handler
+
+ def load(
+ self, dpi: float | tuple[float, float] | None = None
+ ) -> Image.core.PixelAccess | None:
+ if dpi is not None:
+ self.info["dpi"] = dpi
+ x0, y0, x1, y1 = self.info["wmf_bbox"]
+ if not isinstance(dpi, tuple):
+ dpi = dpi, dpi
+ self._size = (
+ int((x1 - x0) * dpi[0] / self._inch[0]),
+ int((y1 - y0) * dpi[1] / self._inch[1]),
+ )
+ return super().load()
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if _handler is None or not hasattr(_handler, "save"):
+ msg = "WMF save handler not installed"
+ raise OSError(msg)
+ _handler.save(im, fp, filename)
+
+
+#
+# --------------------------------------------------------------------
+# Registry stuff
+
+
+Image.register_open(WmfStubImageFile.format, WmfStubImageFile, _accept)
+Image.register_save(WmfStubImageFile.format, _save)
+
+Image.register_extensions(WmfStubImageFile.format, [".wmf", ".emf"])
diff --git a/venv/lib/python3.11/site-packages/PIL/XVThumbImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/XVThumbImagePlugin.py
new file mode 100644
index 000000000..192c041d9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/XVThumbImagePlugin.py
@@ -0,0 +1,83 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# XV Thumbnail file handler by Charles E. "Gene" Cash
+# (gcash@magicnet.net)
+#
+# see xvcolor.c and xvbrowse.c in the sources to John Bradley's XV,
+# available from ftp://ftp.cis.upenn.edu/pub/xv/
+#
+# history:
+# 98-08-15 cec created (b/w only)
+# 98-12-09 cec added color palette
+# 98-12-28 fl added to PIL (with only a few very minor modifications)
+#
+# To do:
+# FIXME: make save work (this requires quantization support)
+#
+from __future__ import annotations
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import o8
+
+_MAGIC = b"P7 332"
+
+# standard color palette for thumbnails (RGB332)
+PALETTE = b""
+for r in range(8):
+ for g in range(8):
+ for b in range(4):
+ PALETTE = PALETTE + (
+ o8((r * 255) // 7) + o8((g * 255) // 7) + o8((b * 255) // 3)
+ )
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(_MAGIC)
+
+
+##
+# Image plugin for XV thumbnail images.
+
+
+class XVThumbImageFile(ImageFile.ImageFile):
+ format = "XVThumb"
+ format_description = "XV thumbnail image"
+
+ def _open(self) -> None:
+ # check magic
+ assert self.fp is not None
+
+ if not _accept(self.fp.read(6)):
+ msg = "not an XV thumbnail file"
+ raise SyntaxError(msg)
+
+ # Skip to beginning of next line
+ self.fp.readline()
+
+ # skip info comments
+ while True:
+ s = self.fp.readline()
+ if not s:
+ msg = "Unexpected EOF reading XV thumbnail file"
+ raise SyntaxError(msg)
+ if s[0] != 35: # ie. when not a comment: '#'
+ break
+
+ # parse header line (already read)
+ w, h = s.strip().split(maxsplit=2)[:2]
+
+ self._mode = "P"
+ self._size = int(w), int(h)
+
+ self.palette = ImagePalette.raw("RGB", PALETTE)
+
+ self.tile = [
+ ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), self.mode)
+ ]
+
+
+# --------------------------------------------------------------------
+
+Image.register_open(XVThumbImageFile.format, XVThumbImageFile, _accept)
diff --git a/venv/lib/python3.11/site-packages/PIL/XbmImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/XbmImagePlugin.py
new file mode 100644
index 000000000..1e57aa162
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/XbmImagePlugin.py
@@ -0,0 +1,98 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# XBM File handling
+#
+# History:
+# 1995-09-08 fl Created
+# 1996-11-01 fl Added save support
+# 1997-07-07 fl Made header parser more tolerant
+# 1997-07-22 fl Fixed yet another parser bug
+# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4)
+# 2001-05-13 fl Added hotspot handling (based on code from Bernhard Herzog)
+# 2004-02-24 fl Allow some whitespace before first #define
+#
+# Copyright (c) 1997-2004 by Secret Labs AB
+# Copyright (c) 1996-1997 by Fredrik Lundh
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import re
+from typing import IO
+
+from . import Image, ImageFile
+
+# XBM header
+xbm_head = re.compile(
+ rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+"
+ b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+"
+ b"(?P"
+ b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+"
+ b"#define[ \t]+[^_]*_y_hot[ \t]+(?P[0-9]+)[\r\n]+"
+ b")?"
+ rb"[\000-\377]*_bits\[]"
+)
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.lstrip().startswith(b"#define")
+
+
+##
+# Image plugin for X11 bitmaps.
+
+
+class XbmImageFile(ImageFile.ImageFile):
+ format = "XBM"
+ format_description = "X11 Bitmap"
+
+ def _open(self) -> None:
+ assert self.fp is not None
+
+ m = xbm_head.match(self.fp.read(512))
+
+ if not m:
+ msg = "not a XBM file"
+ raise SyntaxError(msg)
+
+ xsize = int(m.group("width"))
+ ysize = int(m.group("height"))
+
+ if m.group("hotspot"):
+ self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot")))
+
+ self._mode = "1"
+ self._size = xsize, ysize
+
+ self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end())]
+
+
+def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
+ if im.mode != "1":
+ msg = f"cannot write mode {im.mode} as XBM"
+ raise OSError(msg)
+
+ fp.write(f"#define im_width {im.size[0]}\n".encode("ascii"))
+ fp.write(f"#define im_height {im.size[1]}\n".encode("ascii"))
+
+ hotspot = im.encoderinfo.get("hotspot")
+ if hotspot:
+ fp.write(f"#define im_x_hot {hotspot[0]}\n".encode("ascii"))
+ fp.write(f"#define im_y_hot {hotspot[1]}\n".encode("ascii"))
+
+ fp.write(b"static char im_bits[] = {\n")
+
+ ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size)])
+
+ fp.write(b"};\n")
+
+
+Image.register_open(XbmImageFile.format, XbmImageFile, _accept)
+Image.register_save(XbmImageFile.format, _save)
+
+Image.register_extension(XbmImageFile.format, ".xbm")
+
+Image.register_mime(XbmImageFile.format, "image/xbm")
diff --git a/venv/lib/python3.11/site-packages/PIL/XpmImagePlugin.py b/venv/lib/python3.11/site-packages/PIL/XpmImagePlugin.py
new file mode 100644
index 000000000..80192f55e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/XpmImagePlugin.py
@@ -0,0 +1,157 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# XPM File handling
+#
+# History:
+# 1996-12-29 fl Created
+# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7)
+#
+# Copyright (c) Secret Labs AB 1997-2001.
+# Copyright (c) Fredrik Lundh 1996-2001.
+#
+# See the README file for information on usage and redistribution.
+#
+from __future__ import annotations
+
+import re
+
+from . import Image, ImageFile, ImagePalette
+from ._binary import o8
+
+# XPM header
+xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)')
+
+
+def _accept(prefix: bytes) -> bool:
+ return prefix.startswith(b"/* XPM */")
+
+
+##
+# Image plugin for X11 pixel maps.
+
+
+class XpmImageFile(ImageFile.ImageFile):
+ format = "XPM"
+ format_description = "X11 Pixel Map"
+
+ def _open(self) -> None:
+ assert self.fp is not None
+ if not _accept(self.fp.read(9)):
+ msg = "not an XPM file"
+ raise SyntaxError(msg)
+
+ # skip forward to next string
+ while True:
+ line = self.fp.readline()
+ if not line:
+ msg = "broken XPM file"
+ raise SyntaxError(msg)
+ m = xpm_head.match(line)
+ if m:
+ break
+
+ self._size = int(m.group(1)), int(m.group(2))
+
+ palette_length = int(m.group(3))
+ bpp = int(m.group(4))
+
+ #
+ # load palette description
+
+ palette = {}
+
+ for _ in range(palette_length):
+ line = self.fp.readline().rstrip()
+
+ c = line[1 : bpp + 1]
+ s = line[bpp + 1 : -2].split()
+
+ for i in range(0, len(s), 2):
+ if s[i] == b"c":
+ # process colour key
+ rgb = s[i + 1]
+ if rgb == b"None":
+ self.info["transparency"] = c
+ elif rgb.startswith(b"#"):
+ rgb_int = int(rgb[1:], 16)
+ palette[c] = (
+ o8((rgb_int >> 16) & 255)
+ + o8((rgb_int >> 8) & 255)
+ + o8(rgb_int & 255)
+ )
+ else:
+ # unknown colour
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
+ break
+
+ else:
+ # missing colour key
+ msg = "cannot read this XPM file"
+ raise ValueError(msg)
+
+ args: tuple[int, dict[bytes, bytes] | tuple[bytes, ...]]
+ if palette_length > 256:
+ self._mode = "RGB"
+ args = (bpp, palette)
+ else:
+ self._mode = "P"
+ self.palette = ImagePalette.raw("RGB", b"".join(palette.values()))
+ args = (bpp, tuple(palette.keys()))
+
+ self.tile = [ImageFile._Tile("xpm", (0, 0) + self.size, self.fp.tell(), args)]
+
+ def load_read(self, read_bytes: int) -> bytes:
+ #
+ # load all image data in one chunk
+
+ xsize, ysize = self.size
+
+ assert self.fp is not None
+ s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)]
+
+ return b"".join(s)
+
+
+class XpmDecoder(ImageFile.PyDecoder):
+ _pulls_fd = True
+
+ def decode(self, buffer: Image.DecoderInput) -> tuple[int, int]:
+ assert self.fd is not None
+
+ data = bytearray()
+ bpp, palette = self.args
+ dest_length = self.state.xsize * self.state.ysize
+ if self.mode == "RGB":
+ dest_length *= 3
+ pixel_header = False
+ while len(data) < dest_length:
+ line = self.fd.readline()
+ if not line:
+ break
+ if line.rstrip() == b"/* pixels */" and not pixel_header:
+ pixel_header = True
+ continue
+ line = b'"'.join(line.split(b'"')[1:-1])
+ for i in range(0, len(line), bpp):
+ key = line[i : i + bpp]
+ if self.mode == "RGB":
+ data += palette[key]
+ else:
+ data += o8(palette.index(key))
+ self.set_as_raw(bytes(data))
+ return -1, 0
+
+
+#
+# Registry
+
+
+Image.register_open(XpmImageFile.format, XpmImageFile, _accept)
+Image.register_decoder("xpm", XpmDecoder)
+
+Image.register_extension(XpmImageFile.format, ".xpm")
+
+Image.register_mime(XpmImageFile.format, "image/xpm")
diff --git a/venv/lib/python3.11/site-packages/PIL/__init__.py b/venv/lib/python3.11/site-packages/PIL/__init__.py
new file mode 100644
index 000000000..faf3e76e0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/__init__.py
@@ -0,0 +1,87 @@
+"""Pillow (Fork of the Python Imaging Library)
+
+Pillow is the friendly PIL fork by Jeffrey 'Alex' Clark and contributors.
+ https://github.com/python-pillow/Pillow/
+
+Pillow is forked from PIL 1.1.7.
+
+PIL is the Python Imaging Library by Fredrik Lundh and contributors.
+Copyright (c) 1999 by Secret Labs AB.
+
+Use PIL.__version__ for this Pillow version.
+
+;-)
+"""
+
+from __future__ import annotations
+
+from . import _version
+
+# VERSION was removed in Pillow 6.0.0.
+# PILLOW_VERSION was removed in Pillow 9.0.0.
+# Use __version__ instead.
+__version__ = _version.__version__
+del _version
+
+
+_plugins = [
+ "AvifImagePlugin",
+ "BlpImagePlugin",
+ "BmpImagePlugin",
+ "BufrStubImagePlugin",
+ "CurImagePlugin",
+ "DcxImagePlugin",
+ "DdsImagePlugin",
+ "EpsImagePlugin",
+ "FitsImagePlugin",
+ "FliImagePlugin",
+ "FpxImagePlugin",
+ "FtexImagePlugin",
+ "GbrImagePlugin",
+ "GifImagePlugin",
+ "GribStubImagePlugin",
+ "Hdf5StubImagePlugin",
+ "IcnsImagePlugin",
+ "IcoImagePlugin",
+ "ImImagePlugin",
+ "ImtImagePlugin",
+ "IptcImagePlugin",
+ "JpegImagePlugin",
+ "Jpeg2KImagePlugin",
+ "McIdasImagePlugin",
+ "MicImagePlugin",
+ "MpegImagePlugin",
+ "MpoImagePlugin",
+ "MspImagePlugin",
+ "PalmImagePlugin",
+ "PcdImagePlugin",
+ "PcxImagePlugin",
+ "PdfImagePlugin",
+ "PixarImagePlugin",
+ "PngImagePlugin",
+ "PpmImagePlugin",
+ "PsdImagePlugin",
+ "QoiImagePlugin",
+ "SgiImagePlugin",
+ "SpiderImagePlugin",
+ "SunImagePlugin",
+ "TgaImagePlugin",
+ "TiffImagePlugin",
+ "WebPImagePlugin",
+ "WmfImagePlugin",
+ "XbmImagePlugin",
+ "XpmImagePlugin",
+ "XVThumbImagePlugin",
+]
+
+
+class UnidentifiedImageError(OSError):
+ """
+ Raised in :py:meth:`PIL.Image.open` if an image cannot be opened and identified.
+
+ If a PNG image raises this error, setting :data:`.ImageFile.LOAD_TRUNCATED_IMAGES`
+ to true may allow the image to be opened after all. The setting will ignore missing
+ data and checksum failures.
+ """
+
+ pass
diff --git a/venv/lib/python3.11/site-packages/PIL/__main__.py b/venv/lib/python3.11/site-packages/PIL/__main__.py
new file mode 100644
index 000000000..043156e89
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/__main__.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+import sys
+
+from .features import pilinfo
+
+pilinfo(supported_formats="--report" not in sys.argv)
diff --git a/venv/lib/python3.11/site-packages/PIL/_avif.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_avif.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..7c04429d5
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_avif.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_avif.pyi b/venv/lib/python3.11/site-packages/PIL/_avif.pyi
new file mode 100644
index 000000000..e27843e53
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_avif.pyi
@@ -0,0 +1,3 @@
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/_binary.py b/venv/lib/python3.11/site-packages/PIL/_binary.py
new file mode 100644
index 000000000..d3236c17a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_binary.py
@@ -0,0 +1,113 @@
+#
+# The Python Imaging Library.
+# $Id$
+#
+# Binary input/output support routines.
+#
+# Copyright (c) 1997-2003 by Secret Labs AB
+# Copyright (c) 1995-2003 by Fredrik Lundh
+# Copyright (c) 2012 by Brian Crowell
+#
+# See the README file for information on usage and redistribution.
+#
+
+
+"""Binary input/output support routines."""
+
+from __future__ import annotations
+
+from struct import pack, unpack_from
+
+
+def i8(c: bytes) -> int:
+ return c[0]
+
+
+def o8(i: int) -> bytes:
+ return bytes((i & 255,))
+
+
+# Input, le = little endian, be = big endian
+def i16le(c: bytes, o: int = 0) -> int:
+ """
+ Converts a 2-bytes (16 bits) string to an unsigned integer.
+
+ :param c: string containing bytes to convert
+ :param o: offset of bytes to convert in string
+ """
+ return unpack_from(" int:
+ """
+ Converts a 2-bytes (16 bits) string to a signed integer.
+
+ :param c: string containing bytes to convert
+ :param o: offset of bytes to convert in string
+ """
+ return unpack_from(" int:
+ """
+ Converts a 2-bytes (16 bits) string to a signed integer, big endian.
+
+ :param c: string containing bytes to convert
+ :param o: offset of bytes to convert in string
+ """
+ return unpack_from(">h", c, o)[0]
+
+
+def i32le(c: bytes, o: int = 0) -> int:
+ """
+ Converts a 4-bytes (32 bits) string to an unsigned integer.
+
+ :param c: string containing bytes to convert
+ :param o: offset of bytes to convert in string
+ """
+ return unpack_from(" int:
+ """
+ Converts a 4-bytes (32 bits) string to a signed integer.
+
+ :param c: string containing bytes to convert
+ :param o: offset of bytes to convert in string
+ """
+ return unpack_from(" int:
+ """
+ Converts a 4-bytes (32 bits) string to a signed integer, big endian.
+
+ :param c: string containing bytes to convert
+ :param o: offset of bytes to convert in string
+ """
+ return unpack_from(">i", c, o)[0]
+
+
+def i16be(c: bytes, o: int = 0) -> int:
+ return unpack_from(">H", c, o)[0]
+
+
+def i32be(c: bytes, o: int = 0) -> int:
+ return unpack_from(">I", c, o)[0]
+
+
+# Output, le = little endian, be = big endian
+def o16le(i: int) -> bytes:
+ return pack(" bytes:
+ return pack(" bytes:
+ return pack(">H", i)
+
+
+def o32be(i: int) -> bytes:
+ return pack(">I", i)
diff --git a/venv/lib/python3.11/site-packages/PIL/_deprecate.py b/venv/lib/python3.11/site-packages/PIL/_deprecate.py
new file mode 100644
index 000000000..711c62ab2
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_deprecate.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import warnings
+
+from . import __version__
+
+
+def deprecate(
+ deprecated: str,
+ when: int | None,
+ replacement: str | None = None,
+ *,
+ action: str | None = None,
+ plural: bool = False,
+ stacklevel: int = 3,
+) -> None:
+ """
+ Deprecations helper.
+
+ :param deprecated: Name of thing to be deprecated.
+ :param when: Pillow major version to be removed in.
+ :param replacement: Name of replacement.
+ :param action: Instead of "replacement", give a custom call to action
+ e.g. "Upgrade to new thing".
+ :param plural: if the deprecated thing is plural, needing "are" instead of "is".
+
+ Usually of the form:
+
+ "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd).
+ Use [replacement] instead."
+
+ You can leave out the replacement sentence:
+
+ "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd)"
+
+ Or with another call to action:
+
+ "[deprecated] is deprecated and will be removed in Pillow [when] (yyyy-mm-dd).
+ [action]."
+ """
+
+ is_ = "are" if plural else "is"
+
+ if when is None:
+ removed = "a future version"
+ elif when <= int(__version__.split(".")[0]):
+ msg = f"{deprecated} {is_} deprecated and should be removed."
+ raise RuntimeError(msg)
+ elif when == 13:
+ removed = "Pillow 13 (2026-10-15)"
+ elif when == 14:
+ removed = "Pillow 14 (2027-10-15)"
+ else:
+ msg = f"Unknown removal version: {when}. Update {__name__}?"
+ raise ValueError(msg)
+
+ if replacement and action:
+ msg = "Use only one of 'replacement' and 'action'"
+ raise ValueError(msg)
+
+ if replacement:
+ action = f". Use {replacement} instead."
+ elif action:
+ action = f". {action.rstrip('.')}."
+ else:
+ action = ""
+
+ warnings.warn(
+ f"{deprecated} {is_} deprecated and will be removed in {removed}{action}",
+ DeprecationWarning,
+ stacklevel=stacklevel,
+ )
diff --git a/venv/lib/python3.11/site-packages/PIL/_imaging.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_imaging.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..f84811761
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_imaging.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_imaging.pyi b/venv/lib/python3.11/site-packages/PIL/_imaging.pyi
new file mode 100644
index 000000000..81028a596
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_imaging.pyi
@@ -0,0 +1,31 @@
+from typing import Any
+
+class ImagingCore:
+ def __getitem__(self, index: int) -> float | tuple[int, ...] | None: ...
+ def __getattr__(self, name: str) -> Any: ...
+
+class ImagingFont:
+ def __getattr__(self, name: str) -> Any: ...
+
+class ImagingDraw:
+ def __getattr__(self, name: str) -> Any: ...
+
+class PixelAccess:
+ def __getitem__(self, xy: tuple[int, int]) -> float | tuple[int, ...]: ...
+ def __setitem__(
+ self, xy: tuple[int, int], color: float | tuple[int, ...]
+ ) -> None: ...
+
+class ImagingDecoder:
+ def __getattr__(self, name: str) -> Any: ...
+
+class ImagingEncoder:
+ def __getattr__(self, name: str) -> Any: ...
+
+class _Outline:
+ def close(self) -> None: ...
+ def __getattr__(self, name: str) -> Any: ...
+
+def font(image: ImagingCore, glyphdata: bytes) -> ImagingFont: ...
+def outline() -> _Outline: ...
+def __getattr__(name: str) -> Any: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..e3d97876f
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_imagingcms.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingcms.pyi b/venv/lib/python3.11/site-packages/PIL/_imagingcms.pyi
new file mode 100644
index 000000000..4fc0d60ab
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_imagingcms.pyi
@@ -0,0 +1,143 @@
+import datetime
+import sys
+from typing import Literal, SupportsFloat, TypeAlias, TypedDict
+
+from ._typing import CapsuleType
+
+littlecms_version: str | None
+
+_Tuple3f: TypeAlias = tuple[float, float, float]
+_Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f]
+_Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f]
+
+class _IccMeasurementCondition(TypedDict):
+ observer: int
+ backing: _Tuple3f
+ geo: str
+ flare: float
+ illuminant_type: str
+
+class _IccViewingCondition(TypedDict):
+ illuminant: _Tuple3f
+ surround: _Tuple3f
+ illuminant_type: str
+
+class CmsProfile:
+ @property
+ def rendering_intent(self) -> int: ...
+ @property
+ def creation_date(self) -> datetime.datetime | None: ...
+ @property
+ def copyright(self) -> str | None: ...
+ @property
+ def target(self) -> str | None: ...
+ @property
+ def manufacturer(self) -> str | None: ...
+ @property
+ def model(self) -> str | None: ...
+ @property
+ def profile_description(self) -> str | None: ...
+ @property
+ def screening_description(self) -> str | None: ...
+ @property
+ def viewing_condition(self) -> str | None: ...
+ @property
+ def version(self) -> float: ...
+ @property
+ def icc_version(self) -> int: ...
+ @property
+ def attributes(self) -> int: ...
+ @property
+ def header_flags(self) -> int: ...
+ @property
+ def header_manufacturer(self) -> str: ...
+ @property
+ def header_model(self) -> str: ...
+ @property
+ def device_class(self) -> str: ...
+ @property
+ def connection_space(self) -> str: ...
+ @property
+ def xcolor_space(self) -> str: ...
+ @property
+ def profile_id(self) -> bytes: ...
+ @property
+ def is_matrix_shaper(self) -> bool: ...
+ @property
+ def technology(self) -> str | None: ...
+ @property
+ def colorimetric_intent(self) -> str | None: ...
+ @property
+ def perceptual_rendering_intent_gamut(self) -> str | None: ...
+ @property
+ def saturation_rendering_intent_gamut(self) -> str | None: ...
+ @property
+ def red_colorant(self) -> _Tuple2x3f | None: ...
+ @property
+ def green_colorant(self) -> _Tuple2x3f | None: ...
+ @property
+ def blue_colorant(self) -> _Tuple2x3f | None: ...
+ @property
+ def red_primary(self) -> _Tuple2x3f | None: ...
+ @property
+ def green_primary(self) -> _Tuple2x3f | None: ...
+ @property
+ def blue_primary(self) -> _Tuple2x3f | None: ...
+ @property
+ def media_white_point_temperature(self) -> float | None: ...
+ @property
+ def media_white_point(self) -> _Tuple2x3f | None: ...
+ @property
+ def media_black_point(self) -> _Tuple2x3f | None: ...
+ @property
+ def luminance(self) -> _Tuple2x3f | None: ...
+ @property
+ def chromatic_adaptation(self) -> tuple[_Tuple3x3f, _Tuple3x3f] | None: ...
+ @property
+ def chromaticity(self) -> _Tuple3x3f | None: ...
+ @property
+ def colorant_table(self) -> list[str] | None: ...
+ @property
+ def colorant_table_out(self) -> list[str] | None: ...
+ @property
+ def intent_supported(self) -> dict[int, tuple[bool, bool, bool]] | None: ...
+ @property
+ def clut(self) -> dict[int, tuple[bool, bool, bool]] | None: ...
+ @property
+ def icc_measurement_condition(self) -> _IccMeasurementCondition | None: ...
+ @property
+ def icc_viewing_condition(self) -> _IccViewingCondition | None: ...
+ def is_intent_supported(self, intent: int, direction: int, /) -> int: ...
+
+class CmsTransform:
+ def apply(self, id_in: CapsuleType, id_out: CapsuleType) -> int: ...
+
+def profile_open(profile: str, /) -> CmsProfile: ...
+def profile_frombytes(profile: bytes, /) -> CmsProfile: ...
+def profile_tobytes(profile: CmsProfile, /) -> bytes: ...
+def buildTransform(
+ input_profile: CmsProfile,
+ output_profile: CmsProfile,
+ in_mode: str,
+ out_mode: str,
+ rendering_intent: int = 0,
+ cms_flags: int = 0,
+ /,
+) -> CmsTransform: ...
+def buildProofTransform(
+ input_profile: CmsProfile,
+ output_profile: CmsProfile,
+ proof_profile: CmsProfile,
+ in_mode: str,
+ out_mode: str,
+ rendering_intent: int = 0,
+ proof_intent: int = 0,
+ cms_flags: int = 0,
+ /,
+) -> CmsTransform: ...
+def createProfile(
+ color_space: Literal["LAB", "XYZ", "sRGB"], color_temp: SupportsFloat = 0.0, /
+) -> CmsProfile: ...
+
+if sys.platform == "win32":
+ def get_display_profile_win32(handle: int = 0, is_dc: int = 0, /) -> str | None: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingft.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_imagingft.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..97eab0484
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_imagingft.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingft.pyi b/venv/lib/python3.11/site-packages/PIL/_imagingft.pyi
new file mode 100644
index 000000000..2136810ba
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_imagingft.pyi
@@ -0,0 +1,70 @@
+from collections.abc import Callable
+from typing import Any
+
+from . import ImageFont, _imaging
+
+class Font:
+ @property
+ def family(self) -> str | None: ...
+ @property
+ def style(self) -> str | None: ...
+ @property
+ def ascent(self) -> int: ...
+ @property
+ def descent(self) -> int: ...
+ @property
+ def height(self) -> int: ...
+ @property
+ def x_ppem(self) -> int: ...
+ @property
+ def y_ppem(self) -> int: ...
+ @property
+ def glyphs(self) -> int: ...
+ def render(
+ self,
+ string: str | bytes,
+ fill: Callable[[int, int], _imaging.ImagingCore],
+ mode: str,
+ dir: str | None,
+ features: list[str] | None,
+ lang: str | None,
+ stroke_width: float,
+ stroke_filled: bool,
+ anchor: str | None,
+ foreground_ink_long: int,
+ start: tuple[float, float],
+ /,
+ ) -> tuple[_imaging.ImagingCore, tuple[int, int]]: ...
+ def getsize(
+ self,
+ string: str | bytes | bytearray,
+ mode: str,
+ dir: str | None,
+ features: list[str] | None,
+ lang: str | None,
+ anchor: str | None,
+ /,
+ ) -> tuple[tuple[int, int], tuple[int, int]]: ...
+ def getlength(
+ self,
+ string: str | bytes,
+ mode: str,
+ dir: str | None,
+ features: list[str] | None,
+ lang: str | None,
+ /,
+ ) -> float: ...
+ def getvarnames(self) -> list[bytes]: ...
+ def getvaraxes(self) -> list[ImageFont.Axis]: ...
+ def setvarname(self, instance_index: int, /) -> None: ...
+ def setvaraxes(self, axes: list[float], /) -> None: ...
+
+def getfont(
+ filename: str | bytes,
+ size: float,
+ index: int,
+ encoding: str,
+ font_bytes: bytes,
+ layout_engine: int,
+) -> Font: ...
+def __getattr__(name: str) -> Any: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..7ddcc65f7
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_imagingmath.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingmath.pyi b/venv/lib/python3.11/site-packages/PIL/_imagingmath.pyi
new file mode 100644
index 000000000..e27843e53
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_imagingmath.pyi
@@ -0,0 +1,3 @@
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingmorph.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_imagingmorph.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..5a540fb85
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_imagingmorph.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingmorph.pyi b/venv/lib/python3.11/site-packages/PIL/_imagingmorph.pyi
new file mode 100644
index 000000000..e27843e53
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_imagingmorph.pyi
@@ -0,0 +1,3 @@
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingtk.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_imagingtk.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..8bd49eb5e
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_imagingtk.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_imagingtk.pyi b/venv/lib/python3.11/site-packages/PIL/_imagingtk.pyi
new file mode 100644
index 000000000..e27843e53
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_imagingtk.pyi
@@ -0,0 +1,3 @@
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/_tkinter_finder.py b/venv/lib/python3.11/site-packages/PIL/_tkinter_finder.py
new file mode 100644
index 000000000..9c0143003
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_tkinter_finder.py
@@ -0,0 +1,20 @@
+"""Find compiled module linking to Tcl / Tk libraries"""
+
+from __future__ import annotations
+
+import sys
+import tkinter
+
+tk = getattr(tkinter, "_tkinter")
+
+try:
+ if hasattr(sys, "pypy_find_executable"):
+ TKINTER_LIB = tk.tklib_cffi.__file__
+ else:
+ TKINTER_LIB = tk.__file__
+except AttributeError:
+ # _tkinter may be compiled directly into Python, in which case __file__ is
+ # not available. load_tkinter_funcs will check the binary first in any case.
+ TKINTER_LIB = None
+
+tk_version = str(tkinter.TkVersion)
diff --git a/venv/lib/python3.11/site-packages/PIL/_typing.py b/venv/lib/python3.11/site-packages/PIL/_typing.py
new file mode 100644
index 000000000..a941f8980
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_typing.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import os
+import sys
+from collections.abc import Sequence
+from typing import Any, Protocol, TypeVar
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from numbers import _IntegralLike as IntegralLike
+
+ try:
+ import numpy.typing as npt
+
+ NumpyArray = npt.NDArray[Any]
+ except ImportError:
+ pass
+
+if sys.version_info >= (3, 13):
+ from types import CapsuleType
+else:
+ CapsuleType = object
+
+if sys.version_info >= (3, 12):
+ from collections.abc import Buffer
+else:
+ Buffer = Any
+
+
+_Ink = float | tuple[int, ...] | str
+
+Coords = Sequence[float] | Sequence[Sequence[float]]
+
+
+_T_co = TypeVar("_T_co", covariant=True)
+
+
+class SupportsRead(Protocol[_T_co]):
+ def read(self, length: int = ..., /) -> _T_co: ...
+
+
+StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes]
+
+
+__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"]
diff --git a/venv/lib/python3.11/site-packages/PIL/_util.py b/venv/lib/python3.11/site-packages/PIL/_util.py
new file mode 100644
index 000000000..b1fa6a0f3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_util.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+import os
+
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Any, NoReturn, TypeGuard
+
+ from ._typing import StrOrBytesPath
+
+
+def is_path(f: Any) -> TypeGuard[StrOrBytesPath]:
+ return isinstance(f, (bytes, str, os.PathLike))
+
+
+class DeferredError:
+ def __init__(self, ex: BaseException):
+ self.ex = ex
+
+ def __getattr__(self, elt: str) -> NoReturn:
+ raise self.ex
+
+ @staticmethod
+ def new(ex: BaseException) -> Any:
+ """
+ Creates an object that raises the wrapped exception ``ex`` when used,
+ and casts it to :py:obj:`~typing.Any` type.
+ """
+ return DeferredError(ex)
diff --git a/venv/lib/python3.11/site-packages/PIL/_version.py b/venv/lib/python3.11/site-packages/PIL/_version.py
new file mode 100644
index 000000000..6a8a6f65e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_version.py
@@ -0,0 +1,4 @@
+# Master version for Pillow
+from __future__ import annotations
+
+__version__ = "12.3.0"
diff --git a/venv/lib/python3.11/site-packages/PIL/_webp.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/PIL/_webp.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..5370df873
Binary files /dev/null and b/venv/lib/python3.11/site-packages/PIL/_webp.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/PIL/_webp.pyi b/venv/lib/python3.11/site-packages/PIL/_webp.pyi
new file mode 100644
index 000000000..e27843e53
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/_webp.pyi
@@ -0,0 +1,3 @@
+from typing import Any
+
+def __getattr__(name: str) -> Any: ...
diff --git a/venv/lib/python3.11/site-packages/PIL/features.py b/venv/lib/python3.11/site-packages/PIL/features.py
new file mode 100644
index 000000000..ff32c2510
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/features.py
@@ -0,0 +1,343 @@
+from __future__ import annotations
+
+import collections
+import os
+import sys
+import warnings
+from typing import IO
+
+import PIL
+
+from . import Image
+
+modules = {
+ "pil": ("PIL._imaging", "PILLOW_VERSION"),
+ "tkinter": ("PIL._tkinter_finder", "tk_version"),
+ "freetype2": ("PIL._imagingft", "freetype2_version"),
+ "littlecms2": ("PIL._imagingcms", "littlecms_version"),
+ "webp": ("PIL._webp", "webpdecoder_version"),
+ "avif": ("PIL._avif", "libavif_version"),
+}
+
+
+def check_module(feature: str) -> bool:
+ """
+ Checks if a module is available.
+
+ :param feature: The module to check for.
+ :returns: ``True`` if available, ``False`` otherwise.
+ :raises ValueError: If the module is not defined in this version of Pillow.
+ """
+ if feature not in modules:
+ msg = f"Unknown module {feature}"
+ raise ValueError(msg)
+
+ module, ver = modules[feature]
+
+ try:
+ __import__(module)
+ return True
+ except ModuleNotFoundError:
+ return False
+ except ImportError as ex:
+ warnings.warn(str(ex))
+ return False
+
+
+def version_module(feature: str) -> str | None:
+ """
+ :param feature: The module to check for.
+ :returns:
+ The loaded version number as a string, or ``None`` if unknown or not available.
+ :raises ValueError: If the module is not defined in this version of Pillow.
+ """
+ if not check_module(feature):
+ return None
+
+ module, ver = modules[feature]
+
+ return getattr(__import__(module, fromlist=[ver]), ver)
+
+
+def get_supported_modules() -> list[str]:
+ """
+ :returns: A list of all supported modules.
+ """
+ return [f for f in modules if check_module(f)]
+
+
+codecs = {
+ "jpg": ("jpeg", "jpeglib"),
+ "jpg_2000": ("jpeg2k", "jp2klib"),
+ "zlib": ("zip", "zlib"),
+ "libtiff": ("libtiff", "libtiff"),
+}
+
+
+def check_codec(feature: str) -> bool:
+ """
+ Checks if a codec is available.
+
+ :param feature: The codec to check for.
+ :returns: ``True`` if available, ``False`` otherwise.
+ :raises ValueError: If the codec is not defined in this version of Pillow.
+ """
+ if feature not in codecs:
+ msg = f"Unknown codec {feature}"
+ raise ValueError(msg)
+
+ codec, lib = codecs[feature]
+
+ return f"{codec}_encoder" in dir(Image.core)
+
+
+def version_codec(feature: str) -> str | None:
+ """
+ :param feature: The codec to check for.
+ :returns:
+ The version number as a string, or ``None`` if not available.
+ Checked at compile time for ``jpg``, run-time otherwise.
+ :raises ValueError: If the codec is not defined in this version of Pillow.
+ """
+ if not check_codec(feature):
+ return None
+
+ codec, lib = codecs[feature]
+
+ version = getattr(Image.core, f"{lib}_version")
+
+ if feature == "libtiff":
+ return version.split("\n")[0].split("Version ")[1]
+
+ return version
+
+
+def get_supported_codecs() -> list[str]:
+ """
+ :returns: A list of all supported codecs.
+ """
+ return [f for f in codecs if check_codec(f)]
+
+
+features: dict[str, tuple[str, str, str | None]] = {
+ "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"),
+ "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"),
+ "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"),
+ "libjpeg_turbo": ("PIL._imaging", "HAVE_LIBJPEGTURBO", "libjpeg_turbo_version"),
+ "mozjpeg": ("PIL._imaging", "HAVE_MOZJPEG", "libjpeg_turbo_version"),
+ "zlib_ng": ("PIL._imaging", "HAVE_ZLIBNG", "zlib_ng_version"),
+ "libimagequant": ("PIL._imaging", "HAVE_LIBIMAGEQUANT", "imagequant_version"),
+ "xcb": ("PIL._imaging", "HAVE_XCB", None),
+}
+
+
+def check_feature(feature: str) -> bool | None:
+ """
+ Checks if a feature is available.
+
+ :param feature: The feature to check for.
+ :returns: ``True`` if available, ``False`` if unavailable, ``None`` if unknown.
+ :raises ValueError: If the feature is not defined in this version of Pillow.
+ """
+ if feature not in features:
+ msg = f"Unknown feature {feature}"
+ raise ValueError(msg)
+
+ module, flag, ver = features[feature]
+
+ try:
+ imported_module = __import__(module, fromlist=["PIL"])
+ return getattr(imported_module, flag)
+ except ModuleNotFoundError:
+ return None
+ except ImportError as ex:
+ warnings.warn(str(ex))
+ return None
+
+
+def version_feature(feature: str) -> str | None:
+ """
+ :param feature: The feature to check for.
+ :returns: The version number as a string, or ``None`` if not available.
+ :raises ValueError: If the feature is not defined in this version of Pillow.
+ """
+ if not check_feature(feature):
+ return None
+
+ module, flag, ver = features[feature]
+
+ if ver is None:
+ return None
+
+ return getattr(__import__(module, fromlist=[ver]), ver)
+
+
+def get_supported_features() -> list[str]:
+ """
+ :returns: A list of all supported features.
+ """
+ return [f for f in features if check_feature(f)]
+
+
+def check(feature: str) -> bool | None:
+ """
+ :param feature: A module, codec, or feature name.
+ :returns:
+ ``True`` if the module, codec, or feature is available,
+ ``False`` or ``None`` otherwise.
+ """
+
+ if feature in modules:
+ return check_module(feature)
+ if feature in codecs:
+ return check_codec(feature)
+ if feature in features:
+ return check_feature(feature)
+ warnings.warn(f"Unknown feature '{feature}'.", stacklevel=2)
+ return False
+
+
+def version(feature: str) -> str | None:
+ """
+ :param feature:
+ The module, codec, or feature to check for.
+ :returns:
+ The version number as a string, or ``None`` if unknown or not available.
+ """
+ if feature in modules:
+ return version_module(feature)
+ if feature in codecs:
+ return version_codec(feature)
+ if feature in features:
+ return version_feature(feature)
+ return None
+
+
+def get_supported() -> list[str]:
+ """
+ :returns: A list of all supported modules, features, and codecs.
+ """
+
+ ret = get_supported_modules()
+ ret.extend(get_supported_features())
+ ret.extend(get_supported_codecs())
+ return ret
+
+
+def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None:
+ """
+ Prints information about this installation of Pillow.
+ This function can be called with ``python3 -m PIL``.
+ It can also be called with ``python3 -m PIL.report`` or ``python3 -m PIL --report``
+ to have "supported_formats" set to ``False``, omitting the list of all supported
+ image file formats.
+
+ :param out:
+ The output stream to print to. Defaults to ``sys.stdout`` if ``None``.
+ :param supported_formats:
+ If ``True``, a list of all supported image file formats will be printed.
+ """
+
+ if out is None:
+ out = sys.stdout
+
+ Image.init()
+
+ print("-" * 68, file=out)
+ print(f"Pillow {PIL.__version__}", file=out)
+ py_version_lines = sys.version.splitlines()
+ print(f"Python {py_version_lines[0].strip()}", file=out)
+ for py_version in py_version_lines[1:]:
+ print(f" {py_version.strip()}", file=out)
+ print("-" * 68, file=out)
+ print(f"Python executable is {sys.executable or 'unknown'}", file=out)
+ if sys.prefix != sys.base_prefix:
+ print(f"Environment Python files loaded from {sys.prefix}", file=out)
+ print(f"System Python files loaded from {sys.base_prefix}", file=out)
+ print("-" * 68, file=out)
+ print(
+ f"Python Pillow modules loaded from {os.path.dirname(Image.__file__)}",
+ file=out,
+ )
+ print(
+ f"Binary Pillow modules loaded from {os.path.dirname(Image.core.__file__)}",
+ file=out,
+ )
+ print("-" * 68, file=out)
+
+ for name, feature in [
+ ("pil", "PIL CORE"),
+ ("tkinter", "TKINTER"),
+ ("freetype2", "FREETYPE2"),
+ ("littlecms2", "LITTLECMS2"),
+ ("webp", "WEBP"),
+ ("avif", "AVIF"),
+ ("jpg", "JPEG"),
+ ("jpg_2000", "OPENJPEG (JPEG2000)"),
+ ("zlib", "ZLIB (PNG/ZIP)"),
+ ("libtiff", "LIBTIFF"),
+ ("raqm", "RAQM (Bidirectional Text)"),
+ ("libimagequant", "LIBIMAGEQUANT (Quantization method)"),
+ ("xcb", "XCB (X protocol)"),
+ ]:
+ if check(name):
+ v: str | None = None
+ if name == "jpg":
+ libjpeg_turbo_version = version_feature("libjpeg_turbo")
+ if libjpeg_turbo_version is not None:
+ v = "mozjpeg" if check_feature("mozjpeg") else "libjpeg-turbo"
+ v += " " + libjpeg_turbo_version
+ if v is None:
+ v = version(name)
+ if v is not None:
+ version_static = name in ("pil", "jpg")
+ if name == "littlecms2":
+ # this check is also in src/_imagingcms.c:setup_module()
+ version_static = tuple(int(x) for x in v.split(".")) < (2, 7)
+ t = "compiled for" if version_static else "loaded"
+ if name == "zlib":
+ zlib_ng_version = version_feature("zlib_ng")
+ if zlib_ng_version is not None:
+ v += ", compiled for zlib-ng " + zlib_ng_version
+ elif name == "raqm":
+ for f in ("fribidi", "harfbuzz"):
+ v2 = version_feature(f)
+ if v2 is not None:
+ v += f", {f} {v2}"
+ print("---", feature, "support ok,", t, v, file=out)
+ else:
+ print("---", feature, "support ok", file=out)
+ else:
+ print("***", feature, "support not installed", file=out)
+ print("-" * 68, file=out)
+
+ if supported_formats:
+ extensions = collections.defaultdict(list)
+ for ext, i in Image.EXTENSION.items():
+ extensions[i].append(ext)
+
+ for i in sorted(Image.ID):
+ line = f"{i}"
+ if i in Image.MIME:
+ line = f"{line} {Image.MIME[i]}"
+ print(line, file=out)
+
+ if i in extensions:
+ print(
+ "Extensions: {}".format(", ".join(sorted(extensions[i]))), file=out
+ )
+
+ features = []
+ if i in Image.OPEN:
+ features.append("open")
+ if i in Image.SAVE:
+ features.append("save")
+ if i in Image.SAVE_ALL:
+ features.append("save_all")
+ if i in Image.DECODERS:
+ features.append("decode")
+ if i in Image.ENCODERS:
+ features.append("encode")
+
+ print("Features: {}".format(", ".join(features)), file=out)
+ print("-" * 68, file=out)
diff --git a/venv/lib/python3.11/site-packages/PIL/py.typed b/venv/lib/python3.11/site-packages/PIL/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/PIL/report.py b/venv/lib/python3.11/site-packages/PIL/report.py
new file mode 100644
index 000000000..d2815e845
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/PIL/report.py
@@ -0,0 +1,5 @@
+from __future__ import annotations
+
+from .features import pilinfo
+
+pilinfo(supported_formats=False)
diff --git a/venv/lib/python3.11/site-packages/__editable__.google_genai-2.28.0.pth b/venv/lib/python3.11/site-packages/__editable__.google_genai-2.28.0.pth
new file mode 100644
index 000000000..0b63bef8a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/__editable__.google_genai-2.28.0.pth
@@ -0,0 +1 @@
+import __editable___google_genai_2_28_0_finder; __editable___google_genai_2_28_0_finder.install()
\ No newline at end of file
diff --git a/venv/lib/python3.11/site-packages/__editable___google_genai_2_28_0_finder.py b/venv/lib/python3.11/site-packages/__editable___google_genai_2_28_0_finder.py
new file mode 100644
index 000000000..ae03459f4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/__editable___google_genai_2_28_0_finder.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+import sys
+from importlib.machinery import ModuleSpec, PathFinder
+from importlib.machinery import all_suffixes as module_suffixes
+from importlib.util import spec_from_file_location
+from itertools import chain
+from pathlib import Path
+
+MAPPING: dict[str, str] = {'google': '/home/adrian/python-genai/google'}
+NAMESPACES: dict[str, list[str]] = {'google': ['/home/adrian/python-genai/google'], 'google.genai._gaos.lib': ['/home/adrian/python-genai/google/genai/_gaos/lib'], 'google.genai.tests.data': ['/home/adrian/python-genai/google/genai/tests/data'], 'google.genai.tests.imports': ['/home/adrian/python-genai/google/genai/tests/imports']}
+PATH_PLACEHOLDER = '__editable__.google_genai-2.28.0.finder' + ".__path_hook__"
+
+
+class _EditableFinder: # MetaPathFinder
+ @classmethod
+ def find_spec(cls, fullname: str, path=None, target=None) -> ModuleSpec | None: # type: ignore
+ # Top-level packages and modules (we know these exist in the FS)
+ if fullname in MAPPING:
+ pkg_path = MAPPING[fullname]
+ return cls._find_spec(fullname, Path(pkg_path))
+
+ # Handle immediate children modules (required for namespaces to work)
+ # To avoid problems with case sensitivity in the file system we delegate
+ # to the importlib.machinery implementation.
+ parent, _, child = fullname.rpartition(".")
+ if parent and parent in MAPPING:
+ return PathFinder.find_spec(fullname, path=[MAPPING[parent]])
+
+ # Other levels of nesting should be handled automatically by importlib
+ # using the parent path.
+ return None
+
+ @classmethod
+ def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None:
+ init = candidate_path / "__init__.py"
+ candidates = (candidate_path.with_suffix(x) for x in module_suffixes())
+ for candidate in chain([init], candidates):
+ if candidate.exists():
+ return spec_from_file_location(fullname, candidate)
+ return None
+
+
+class _EditableNamespaceFinder: # PathEntryFinder
+ @classmethod
+ def _path_hook(cls, path) -> type[_EditableNamespaceFinder]:
+ if path == PATH_PLACEHOLDER:
+ return cls
+ raise ImportError
+
+ @classmethod
+ def _paths(cls, fullname: str) -> list[str]:
+ paths = NAMESPACES[fullname]
+ if not paths and fullname in MAPPING:
+ paths = [MAPPING[fullname]]
+ # Always add placeholder, for 2 reasons:
+ # 1. __path__ cannot be empty for the spec to be considered namespace.
+ # 2. In the case of nested namespaces, we need to force
+ # import machinery to query _EditableNamespaceFinder again.
+ return [*paths, PATH_PLACEHOLDER]
+
+ @classmethod
+ def find_spec(cls, fullname: str, target=None) -> ModuleSpec | None: # type: ignore
+ if fullname in NAMESPACES:
+ spec = ModuleSpec(fullname, None, is_package=True)
+ spec.submodule_search_locations = cls._paths(fullname)
+ return spec
+ return None
+
+ @classmethod
+ def find_module(cls, _fullname) -> None:
+ return None
+
+
+def install():
+ if not any(finder == _EditableFinder for finder in sys.meta_path):
+ sys.meta_path.append(_EditableFinder)
+
+ if not NAMESPACES:
+ return
+
+ if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks):
+ # PathEntryFinder is needed to create NamespaceSpec without private APIS
+ sys.path_hooks.append(_EditableNamespaceFinder._path_hook)
+ if PATH_PLACEHOLDER not in sys.path:
+ sys.path.append(PATH_PLACEHOLDER) # Used just to trigger the path hook
diff --git a/venv/lib/python3.11/site-packages/_cffi_backend.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/_cffi_backend.cpython-311-x86_64-linux-gnu.so
new file mode 100755
index 000000000..d487bf9f0
Binary files /dev/null and b/venv/lib/python3.11/site-packages/_cffi_backend.cpython-311-x86_64-linux-gnu.so differ
diff --git a/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py b/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py
new file mode 100644
index 000000000..f987a5367
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py
@@ -0,0 +1,222 @@
+# don't import any costly modules
+import sys
+import os
+
+
+is_pypy = '__pypy__' in sys.builtin_module_names
+
+
+def warn_distutils_present():
+ if 'distutils' not in sys.modules:
+ return
+ if is_pypy and sys.version_info < (3, 7):
+ # PyPy for 3.6 unconditionally imports distutils, so bypass the warning
+ # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
+ return
+ import warnings
+
+ warnings.warn(
+ "Distutils was imported before Setuptools, but importing Setuptools "
+ "also replaces the `distutils` module in `sys.modules`. This may lead "
+ "to undesirable behaviors or errors. To avoid these issues, avoid "
+ "using distutils directly, ensure that setuptools is installed in the "
+ "traditional way (e.g. not an editable install), and/or make sure "
+ "that setuptools is always imported before distutils."
+ )
+
+
+def clear_distutils():
+ if 'distutils' not in sys.modules:
+ return
+ import warnings
+
+ warnings.warn("Setuptools is replacing distutils.")
+ mods = [
+ name
+ for name in sys.modules
+ if name == "distutils" or name.startswith("distutils.")
+ ]
+ for name in mods:
+ del sys.modules[name]
+
+
+def enabled():
+ """
+ Allow selection of distutils by environment variable.
+ """
+ which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
+ return which == 'local'
+
+
+def ensure_local_distutils():
+ import importlib
+
+ clear_distutils()
+
+ # With the DistutilsMetaFinder in place,
+ # perform an import to cause distutils to be
+ # loaded from setuptools._distutils. Ref #2906.
+ with shim():
+ importlib.import_module('distutils')
+
+ # check that submodules load as expected
+ core = importlib.import_module('distutils.core')
+ assert '_distutils' in core.__file__, core.__file__
+ assert 'setuptools._distutils.log' not in sys.modules
+
+
+def do_override():
+ """
+ Ensure that the local copy of distutils is preferred over stdlib.
+
+ See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
+ for more motivation.
+ """
+ if enabled():
+ warn_distutils_present()
+ ensure_local_distutils()
+
+
+class _TrivialRe:
+ def __init__(self, *patterns):
+ self._patterns = patterns
+
+ def match(self, string):
+ return all(pat in string for pat in self._patterns)
+
+
+class DistutilsMetaFinder:
+ def find_spec(self, fullname, path, target=None):
+ # optimization: only consider top level modules and those
+ # found in the CPython test suite.
+ if path is not None and not fullname.startswith('test.'):
+ return
+
+ method_name = 'spec_for_{fullname}'.format(**locals())
+ method = getattr(self, method_name, lambda: None)
+ return method()
+
+ def spec_for_distutils(self):
+ if self.is_cpython():
+ return
+
+ import importlib
+ import importlib.abc
+ import importlib.util
+
+ try:
+ mod = importlib.import_module('setuptools._distutils')
+ except Exception:
+ # There are a couple of cases where setuptools._distutils
+ # may not be present:
+ # - An older Setuptools without a local distutils is
+ # taking precedence. Ref #2957.
+ # - Path manipulation during sitecustomize removes
+ # setuptools from the path but only after the hook
+ # has been loaded. Ref #2980.
+ # In either case, fall back to stdlib behavior.
+ return
+
+ class DistutilsLoader(importlib.abc.Loader):
+ def create_module(self, spec):
+ mod.__name__ = 'distutils'
+ return mod
+
+ def exec_module(self, module):
+ pass
+
+ return importlib.util.spec_from_loader(
+ 'distutils', DistutilsLoader(), origin=mod.__file__
+ )
+
+ @staticmethod
+ def is_cpython():
+ """
+ Suppress supplying distutils for CPython (build and tests).
+ Ref #2965 and #3007.
+ """
+ return os.path.isfile('pybuilddir.txt')
+
+ def spec_for_pip(self):
+ """
+ Ensure stdlib distutils when running under pip.
+ See pypa/pip#8761 for rationale.
+ """
+ if self.pip_imported_during_build():
+ return
+ clear_distutils()
+ self.spec_for_distutils = lambda: None
+
+ @classmethod
+ def pip_imported_during_build(cls):
+ """
+ Detect if pip is being imported in a build script. Ref #2355.
+ """
+ import traceback
+
+ return any(
+ cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
+ )
+
+ @staticmethod
+ def frame_file_is_setup(frame):
+ """
+ Return True if the indicated frame suggests a setup.py file.
+ """
+ # some frames may not have __file__ (#2940)
+ return frame.f_globals.get('__file__', '').endswith('setup.py')
+
+ def spec_for_sensitive_tests(self):
+ """
+ Ensure stdlib distutils when running select tests under CPython.
+
+ python/cpython#91169
+ """
+ clear_distutils()
+ self.spec_for_distutils = lambda: None
+
+ sensitive_tests = (
+ [
+ 'test.test_distutils',
+ 'test.test_peg_generator',
+ 'test.test_importlib',
+ ]
+ if sys.version_info < (3, 10)
+ else [
+ 'test.test_distutils',
+ ]
+ )
+
+
+for name in DistutilsMetaFinder.sensitive_tests:
+ setattr(
+ DistutilsMetaFinder,
+ f'spec_for_{name}',
+ DistutilsMetaFinder.spec_for_sensitive_tests,
+ )
+
+
+DISTUTILS_FINDER = DistutilsMetaFinder()
+
+
+def add_shim():
+ DISTUTILS_FINDER in sys.meta_path or insert_shim()
+
+
+class shim:
+ def __enter__(self):
+ insert_shim()
+
+ def __exit__(self, exc, value, tb):
+ remove_shim()
+
+
+def insert_shim():
+ sys.meta_path.insert(0, DISTUTILS_FINDER)
+
+
+def remove_shim():
+ try:
+ sys.meta_path.remove(DISTUTILS_FINDER)
+ except ValueError:
+ pass
diff --git a/venv/lib/python3.11/site-packages/_distutils_hack/override.py b/venv/lib/python3.11/site-packages/_distutils_hack/override.py
new file mode 100644
index 000000000..2cc433a4a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_distutils_hack/override.py
@@ -0,0 +1 @@
+__import__('_distutils_hack').do_override()
diff --git a/venv/lib/python3.11/site-packages/_pytest/__init__.py b/venv/lib/python3.11/site-packages/_pytest/__init__.py
new file mode 100644
index 000000000..8eb8ec960
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/__init__.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+
+__all__ = ["__version__", "version_tuple"]
+
+try:
+ from ._version import version as __version__
+ from ._version import version_tuple
+except ImportError: # pragma: no cover
+ # broken installation, we don't even try
+ # unknown only works because we do poor mans version compare
+ __version__ = "unknown"
+ version_tuple = (0, 0, "unknown")
diff --git a/venv/lib/python3.11/site-packages/_pytest/_argcomplete.py b/venv/lib/python3.11/site-packages/_pytest/_argcomplete.py
new file mode 100644
index 000000000..59426ef94
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_argcomplete.py
@@ -0,0 +1,117 @@
+"""Allow bash-completion for argparse with argcomplete if installed.
+
+Needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
+to find the magic string, so _ARGCOMPLETE env. var is never set, and
+this does not need special code).
+
+Function try_argcomplete(parser) should be called directly before
+the call to ArgumentParser.parse_args().
+
+The filescompleter is what you normally would use on the positional
+arguments specification, in order to get "dirname/" after "dirn"
+instead of the default "dirname ":
+
+ optparser.add_argument(Config._file_or_dir, nargs='*').completer=filescompleter
+
+Other, application specific, completers should go in the file
+doing the add_argument calls as they need to be specified as .completer
+attributes as well. (If argcomplete is not installed, the function the
+attribute points to will not be used).
+
+SPEEDUP
+=======
+
+The generic argcomplete script for bash-completion
+(/etc/bash_completion.d/python-argcomplete.sh)
+uses a python program to determine startup script generated by pip.
+You can speed up completion somewhat by changing this script to include
+ # PYTHON_ARGCOMPLETE_OK
+so the python-argcomplete-check-easy-install-script does not
+need to be called to find the entry point of the code and see if that is
+marked with PYTHON_ARGCOMPLETE_OK.
+
+INSTALL/DEBUGGING
+=================
+
+To include this support in another application that has setup.py generated
+scripts:
+
+- Add the line:
+ # PYTHON_ARGCOMPLETE_OK
+ near the top of the main python entry point.
+
+- Include in the file calling parse_args():
+ from _argcomplete import try_argcomplete, filescompleter
+ Call try_argcomplete just before parse_args(), and optionally add
+ filescompleter to the positional arguments' add_argument().
+
+If things do not work right away:
+
+- Switch on argcomplete debugging with (also helpful when doing custom
+ completers):
+ export _ARC_DEBUG=1
+
+- Run:
+ python-argcomplete-check-easy-install-script $(which appname)
+ echo $?
+ will echo 0 if the magic line has been found, 1 if not.
+
+- Sometimes it helps to find early on errors using:
+ _ARGCOMPLETE=1 _ARC_DEBUG=1 appname
+ which should throw a KeyError: 'COMPLINE' (which is properly set by the
+ global argcomplete script).
+"""
+
+from __future__ import annotations
+
+import argparse
+from glob import glob
+import os
+import sys
+from typing import Any
+
+
+class FastFilesCompleter:
+ """Fast file completer class."""
+
+ def __init__(self, directories: bool = True) -> None:
+ self.directories = directories
+
+ def __call__(self, prefix: str, **kwargs: Any) -> list[str]:
+ # Only called on non option completions.
+ if os.sep in prefix[1:]:
+ prefix_dir = len(os.path.dirname(prefix) + os.sep)
+ else:
+ prefix_dir = 0
+ completion = []
+ globbed = []
+ if "*" not in prefix and "?" not in prefix:
+ # We are on unix, otherwise no bash.
+ if not prefix or prefix[-1] == os.sep:
+ globbed.extend(glob(prefix + ".*"))
+ prefix += "*"
+ globbed.extend(glob(prefix))
+ for x in sorted(globbed):
+ if os.path.isdir(x):
+ x += "/"
+ # Append stripping the prefix (like bash, not like compgen).
+ completion.append(x[prefix_dir:])
+ return completion
+
+
+if os.environ.get("_ARGCOMPLETE"):
+ try:
+ import argcomplete.completers
+ except ImportError:
+ sys.exit(-1)
+ filescompleter: FastFilesCompleter | None = FastFilesCompleter()
+
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
+ argcomplete.autocomplete(parser, always_complete_options=False)
+
+else:
+
+ def try_argcomplete(parser: argparse.ArgumentParser) -> None:
+ pass
+
+ filescompleter = None
diff --git a/venv/lib/python3.11/site-packages/_pytest/_code/__init__.py b/venv/lib/python3.11/site-packages/_pytest/_code/__init__.py
new file mode 100644
index 000000000..7f67a2e3e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_code/__init__.py
@@ -0,0 +1,26 @@
+"""Python inspection/code generation API."""
+
+from __future__ import annotations
+
+from .code import Code
+from .code import ExceptionInfo
+from .code import filter_traceback
+from .code import Frame
+from .code import getfslineno
+from .code import Traceback
+from .code import TracebackEntry
+from .source import getrawcode
+from .source import Source
+
+
+__all__ = [
+ "Code",
+ "ExceptionInfo",
+ "Frame",
+ "Source",
+ "Traceback",
+ "TracebackEntry",
+ "filter_traceback",
+ "getfslineno",
+ "getrawcode",
+]
diff --git a/venv/lib/python3.11/site-packages/_pytest/_code/code.py b/venv/lib/python3.11/site-packages/_pytest/_code/code.py
new file mode 100644
index 000000000..3c453b15d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_code/code.py
@@ -0,0 +1,1632 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+import ast
+from collections.abc import Callable
+from collections.abc import Iterable
+from collections.abc import Mapping
+from collections.abc import Sequence
+import dataclasses
+import inspect
+from io import StringIO
+import os
+from pathlib import Path
+import re
+import sys
+from traceback import extract_tb
+from traceback import format_exception
+from traceback import format_exception_only
+from traceback import FrameSummary
+from types import CodeType
+from types import FrameType
+from types import TracebackType
+from typing import Any
+from typing import ClassVar
+from typing import Final
+from typing import final
+from typing import Generic
+from typing import Literal
+from typing import overload
+from typing import SupportsIndex
+from typing import TypeAlias
+from typing import TypeVar
+
+import pluggy
+
+import _pytest
+from _pytest._code.source import findsource
+from _pytest._code.source import getrawcode
+from _pytest._code.source import getstatementrange_ast
+from _pytest._code.source import Source
+from _pytest._io import TerminalWriter
+from _pytest._io.saferepr import safeformat
+from _pytest._io.saferepr import saferepr
+from _pytest.compat import get_real_func
+from _pytest.deprecated import check_ispytest
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
+
+EXCEPTION_OR_MORE = type[BaseException] | tuple[type[BaseException], ...]
+
+
+class Code:
+ """Wrapper around Python code objects."""
+
+ __slots__ = ("raw",)
+
+ def __init__(self, obj: CodeType) -> None:
+ self.raw = obj
+
+ @classmethod
+ def from_function(cls, obj: object) -> Code:
+ return cls(getrawcode(obj))
+
+ def __eq__(self, other):
+ return self.raw == other.raw
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @property
+ def firstlineno(self) -> int:
+ return self.raw.co_firstlineno - 1
+
+ @property
+ def name(self) -> str:
+ return self.raw.co_name
+
+ @property
+ def path(self) -> Path | str:
+ """Return a path object pointing to source code, or an ``str`` in
+ case of ``OSError`` / non-existing file."""
+ filename = inspect.getfile(self.raw)
+ if not filename:
+ return ""
+ try:
+ p = absolutepath(filename)
+ # maybe don't try this checking
+ if not p.exists():
+ raise OSError("path check failed.")
+ return p
+ except OSError:
+ # XXX maybe try harder like the weird logic
+ # in the standard lib [linecache.updatecache] does?
+ return filename
+
+ @property
+ def fullsource(self) -> Source | None:
+ """Return a _pytest._code.Source object for the full source file of the code."""
+ full, _ = findsource(self.raw)
+ return full
+
+ def source(self) -> Source:
+ """Return a _pytest._code.Source object for the code object's source only."""
+ # return source only for that part of code
+ return Source(self.raw)
+
+ def getargs(self, var: bool = False) -> tuple[str, ...]:
+ """Return a tuple with the argument names for the code object.
+
+ If 'var' is set True also return the names of the variable and
+ keyword arguments when present.
+ """
+ # inspect.getargs merges positional and kwonly into a single list;
+ # co_argcount is needed to exclude kwonly when var=False.
+ args, varargs, varkw = inspect.getargs(self.raw)
+ if not var:
+ return tuple(args[: self.raw.co_argcount])
+ result = list(args)
+ if varargs is not None:
+ result.append(varargs)
+ if varkw is not None:
+ result.append(varkw)
+ return tuple(result)
+
+
+class Frame:
+ """Wrapper around a Python frame holding f_locals and f_globals
+ in which expressions can be evaluated."""
+
+ __slots__ = ("raw",)
+
+ def __init__(self, frame: FrameType) -> None:
+ self.raw = frame
+
+ @property
+ def lineno(self) -> int:
+ return self.raw.f_lineno - 1
+
+ @property
+ def f_globals(self) -> dict[str, Any]:
+ return self.raw.f_globals
+
+ @property
+ def f_locals(self) -> dict[str, Any]:
+ return self.raw.f_locals
+
+ @property
+ def code(self) -> Code:
+ return Code(self.raw.f_code)
+
+ @property
+ def statement(self) -> Source:
+ """Statement this frame is at."""
+ if self.code.fullsource is None:
+ return Source("")
+ return self.code.fullsource.getstatement(self.lineno)
+
+ def eval(self, code, **vars):
+ """Evaluate 'code' in the frame.
+
+ 'vars' are optional additional local variables.
+
+ Returns the result of the evaluation.
+ """
+ f_locals = self.f_locals.copy()
+ f_locals.update(vars)
+ return eval(code, self.f_globals, f_locals)
+
+ def repr(self, object: object) -> str:
+ """Return a 'safe' (non-recursive, one-line) string repr for 'object'."""
+ return saferepr(object)
+
+ def getargs(self, var: bool = False):
+ """Return a list of tuples (name, value) for all arguments.
+
+ If 'var' is set True, also include the variable and keyword arguments
+ when present.
+ """
+ retval = []
+ for arg in self.code.getargs(var):
+ try:
+ retval.append((arg, self.f_locals[arg]))
+ except KeyError:
+ pass # this can occur when using Psyco
+ return retval
+
+
+class TracebackEntry:
+ """A single entry in a Traceback."""
+
+ __slots__ = ("_rawentry", "_repr_style")
+
+ def __init__(
+ self,
+ rawentry: TracebackType,
+ repr_style: Literal["short", "long"] | None = None,
+ ) -> None:
+ self._rawentry: Final = rawentry
+ self._repr_style: Final = repr_style
+
+ def with_repr_style(
+ self, repr_style: Literal["short", "long"] | None
+ ) -> TracebackEntry:
+ return TracebackEntry(self._rawentry, repr_style)
+
+ @property
+ def lineno(self) -> int:
+ return self._rawentry.tb_lineno - 1
+
+ def get_python_framesummary(self) -> FrameSummary:
+ # Python's built-in traceback module implements all the nitty gritty
+ # details to get column numbers of out frames.
+ stack_summary = extract_tb(self._rawentry, limit=1)
+ return stack_summary[0]
+
+ # Column and end line numbers introduced in python 3.11
+ if sys.version_info < (3, 11):
+
+ @property
+ def end_lineno_relative(self) -> int | None:
+ return None
+
+ @property
+ def colno(self) -> int | None:
+ return None
+
+ @property
+ def end_colno(self) -> int | None:
+ return None
+ else:
+
+ @property
+ def end_lineno_relative(self) -> int | None:
+ frame_summary = self.get_python_framesummary()
+ if frame_summary.end_lineno is None: # pragma: no cover
+ return None
+ return frame_summary.end_lineno - 1 - self.frame.code.firstlineno
+
+ @property
+ def colno(self) -> int | None:
+ """Starting byte offset of the expression in the traceback entry."""
+ return self.get_python_framesummary().colno
+
+ @property
+ def end_colno(self) -> int | None:
+ """Ending byte offset of the expression in the traceback entry."""
+ return self.get_python_framesummary().end_colno
+
+ @property
+ def frame(self) -> Frame:
+ return Frame(self._rawentry.tb_frame)
+
+ @property
+ def relline(self) -> int:
+ return self.lineno - self.frame.code.firstlineno
+
+ def __repr__(self) -> str:
+ return f""
+
+ @property
+ def statement(self) -> Source:
+ """_pytest._code.Source object for the current statement."""
+ source = self.frame.code.fullsource
+ assert source is not None
+ return source.getstatement(self.lineno)
+
+ @property
+ def path(self) -> Path | str:
+ """Path to the source code."""
+ return self.frame.code.path
+
+ @property
+ def locals(self) -> dict[str, Any]:
+ """Locals of underlying frame."""
+ return self.frame.f_locals
+
+ def getfirstlinesource(self) -> int:
+ return self.frame.code.firstlineno
+
+ def getsource(
+ self, astcache: dict[str | Path, ast.AST] | None = None
+ ) -> Source | None:
+ """Return failing source code."""
+ # we use the passed in astcache to not reparse asttrees
+ # within exception info printing
+ source = self.frame.code.fullsource
+ if source is None:
+ return None
+ key = astnode = None
+ if astcache is not None:
+ key = self.frame.code.path
+ if key is not None:
+ astnode = astcache.get(key, None)
+ start = self.getfirstlinesource()
+ try:
+ astnode, _, end = getstatementrange_ast(
+ self.lineno, source, astnode=astnode
+ )
+ except SyntaxError:
+ end = self.lineno + 1
+ else:
+ if key is not None and astcache is not None:
+ astcache[key] = astnode
+ return source[start:end]
+
+ source = property(getsource)
+
+ def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool:
+ """Return True if the current frame has a var __tracebackhide__
+ resolving to True.
+
+ If __tracebackhide__ is a callable, it gets called with the
+ ExceptionInfo instance and can decide whether to hide the traceback.
+
+ Mostly for internal use.
+ """
+ tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False
+ for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
+ # in normal cases, f_locals and f_globals are dictionaries
+ # however via `exec(...)` / `eval(...)` they can be other types
+ # (even incorrect types!).
+ # as such, we suppress all exceptions while accessing __tracebackhide__
+ try:
+ tbh = maybe_ns_dct["__tracebackhide__"]
+ except Exception:
+ pass
+ else:
+ break
+ if tbh and callable(tbh):
+ return tbh(excinfo)
+ return tbh
+
+ def __str__(self) -> str:
+ name = self.frame.code.name
+ try:
+ line = str(self.statement).lstrip()
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ line = "???"
+ # This output does not quite match Python's repr for traceback entries,
+ # but changing it to do so would break certain plugins. See
+ # https://github.com/pytest-dev/pytest/pull/7535/ for details.
+ return f" File '{self.path}':{self.lineno + 1} in {name}\n {line}\n"
+
+ @property
+ def name(self) -> str:
+ """co_name of underlying code."""
+ return self.frame.code.raw.co_name
+
+
+class Traceback(list[TracebackEntry]):
+ """Traceback objects encapsulate and offer higher level access to Traceback entries."""
+
+ def __init__(
+ self,
+ tb: TracebackType | Iterable[TracebackEntry],
+ ) -> None:
+ """Initialize from given python traceback object and ExceptionInfo."""
+ if isinstance(tb, TracebackType):
+
+ def f(cur: TracebackType) -> Iterable[TracebackEntry]:
+ cur_: TracebackType | None = cur
+ while cur_ is not None:
+ yield TracebackEntry(cur_)
+ cur_ = cur_.tb_next
+
+ super().__init__(f(tb))
+ else:
+ super().__init__(tb)
+
+ def cut(
+ self,
+ path: os.PathLike[str] | str | None = None,
+ lineno: int | None = None,
+ firstlineno: int | None = None,
+ excludepath: os.PathLike[str] | None = None,
+ ) -> Traceback:
+ """Return a Traceback instance wrapping part of this Traceback.
+
+ By providing any combination of path, lineno and firstlineno, the
+ first frame to start the to-be-returned traceback is determined.
+
+ This allows cutting the first part of a Traceback instance e.g.
+ for formatting reasons (removing some uninteresting bits that deal
+ with handling of the exception/traceback).
+ """
+ path_ = None if path is None else os.fspath(path)
+ excludepath_ = None if excludepath is None else os.fspath(excludepath)
+ for x in self:
+ code = x.frame.code
+ codepath = code.path
+ if path is not None and str(codepath) != path_:
+ continue
+ if (
+ excludepath is not None
+ and isinstance(codepath, Path)
+ and excludepath_ in (str(p) for p in codepath.parents) # type: ignore[operator]
+ ):
+ continue
+ if lineno is not None and x.lineno != lineno:
+ continue
+ if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
+ continue
+ return Traceback(x._rawentry)
+ return self
+
+ @overload
+ def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ...
+
+ @overload
+ def __getitem__(self, key: slice) -> Traceback: ...
+
+ def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback:
+ if isinstance(key, slice):
+ return self.__class__(super().__getitem__(key))
+ else:
+ return super().__getitem__(key)
+
+ def filter(
+ self,
+ excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool],
+ /,
+ ) -> Traceback:
+ """Return a Traceback instance with certain items removed.
+
+ If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
+ which are hidden (see ishidden() above).
+
+ Otherwise, the filter is a function that gets a single argument, a
+ ``TracebackEntry`` instance, and should return True when the item should
+ be added to the ``Traceback``, False when not.
+ """
+ if isinstance(excinfo_or_fn, ExceptionInfo):
+ fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
+ else:
+ fn = excinfo_or_fn
+ return Traceback(filter(fn, self))
+
+ def recursionindex(self) -> int | None:
+ """Return the index of the frame/TracebackEntry where recursion originates if
+ appropriate, None if no recursion occurred."""
+ cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {}
+ for i, entry in enumerate(self):
+ # id for the code.raw is needed to work around
+ # the strange metaprogramming in the decorator lib from pypi
+ # which generates code objects that have hash/value equality
+ # XXX needs a test
+ key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
+ values = cache.setdefault(key, [])
+ # Since Python 3.13 f_locals is a proxy, freeze it.
+ loc = dict(entry.frame.f_locals)
+ if values:
+ for otherloc in values:
+ if otherloc == loc:
+ return i
+ values.append(loc)
+ return None
+
+
+def stringify_exception(
+ exc: BaseException, include_subexception_msg: bool = True
+) -> str:
+ try:
+ notes = getattr(exc, "__notes__", [])
+ except KeyError:
+ # Workaround for https://github.com/python/cpython/issues/98778 on
+ # some 3.10 and 3.11 patch versions.
+ HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
+ if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
+ notes = []
+ else: # pragma: no cover
+ # exception not related to above bug, reraise
+ raise
+ if not include_subexception_msg and isinstance(exc, BaseExceptionGroup):
+ message = exc.message
+ else:
+ message = str(exc)
+
+ return "\n".join(
+ [
+ message,
+ *notes,
+ ]
+ )
+
+
+E = TypeVar("E", bound=BaseException, covariant=True)
+
+
+@final
+@dataclasses.dataclass
+class ExceptionInfo(Generic[E]):
+ """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
+
+ _assert_start_repr: ClassVar = "AssertionError('assert "
+
+ _excinfo: tuple[type[E], E, TracebackType] | None
+ _striptext: str
+ _traceback: Traceback | None
+
+ def __init__(
+ self,
+ excinfo: tuple[type[E], E, TracebackType] | None,
+ striptext: str = "",
+ traceback: Traceback | None = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._excinfo = excinfo
+ self._striptext = striptext
+ self._traceback = traceback
+
+ @classmethod
+ def from_exception(
+ cls,
+ # Ignoring error: "Cannot use a covariant type variable as a parameter".
+ # This is OK to ignore because this class is (conceptually) readonly.
+ # See https://github.com/python/mypy/issues/7049.
+ exception: E, # type: ignore[misc]
+ exprinfo: str | None = None,
+ ) -> ExceptionInfo[E]:
+ """Return an ExceptionInfo for an existing exception.
+
+ The exception must have a non-``None`` ``__traceback__`` attribute,
+ otherwise this function fails with an assertion error. This means that
+ the exception must have been raised, or added a traceback with the
+ :py:meth:`~BaseException.with_traceback()` method.
+
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
+
+ .. versionadded:: 7.4
+ """
+ assert exception.__traceback__, (
+ "Exceptions passed to ExcInfo.from_exception(...)"
+ " must have a non-None __traceback__."
+ )
+ exc_info = (type(exception), exception, exception.__traceback__)
+ return cls.from_exc_info(exc_info, exprinfo)
+
+ @classmethod
+ def from_exc_info(
+ cls,
+ exc_info: tuple[type[E], E, TracebackType],
+ exprinfo: str | None = None,
+ ) -> ExceptionInfo[E]:
+ """Like :func:`from_exception`, but using old-style exc_info tuple."""
+ _striptext = ""
+ if exprinfo is None and isinstance(exc_info[1], AssertionError):
+ exprinfo = getattr(exc_info[1], "msg", None)
+ if exprinfo is None:
+ exprinfo = saferepr(exc_info[1])
+ if exprinfo and exprinfo.startswith(cls._assert_start_repr):
+ _striptext = "AssertionError: "
+
+ return cls(exc_info, _striptext, _ispytest=True)
+
+ @classmethod
+ def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]:
+ """Return an ExceptionInfo matching the current traceback.
+
+ .. warning::
+
+ Experimental API
+
+ :param exprinfo:
+ A text string helping to determine if we should strip
+ ``AssertionError`` from the output. Defaults to the exception
+ message/``__str__()``.
+ """
+ tup = sys.exc_info()
+ assert tup[0] is not None, "no current exception"
+ assert tup[1] is not None, "no current exception"
+ assert tup[2] is not None, "no current exception"
+ exc_info = (tup[0], tup[1], tup[2])
+ return ExceptionInfo.from_exc_info(exc_info, exprinfo)
+
+ @classmethod
+ def for_later(cls) -> ExceptionInfo[E]:
+ """Return an unfilled ExceptionInfo."""
+ return cls(None, _ispytest=True)
+
+ def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None:
+ """Fill an unfilled ExceptionInfo created with ``for_later()``."""
+ assert self._excinfo is None, "ExceptionInfo was already filled"
+ self._excinfo = exc_info
+
+ @property
+ def type(self) -> type[E]:
+ """The exception class."""
+ assert self._excinfo is not None, (
+ ".type can only be used after the context manager exits"
+ )
+ return self._excinfo[0]
+
+ @property
+ def value(self) -> E:
+ """The exception value."""
+ assert self._excinfo is not None, (
+ ".value can only be used after the context manager exits"
+ )
+ return self._excinfo[1]
+
+ @property
+ def tb(self) -> TracebackType:
+ """The exception raw traceback."""
+ assert self._excinfo is not None, (
+ ".tb can only be used after the context manager exits"
+ )
+ return self._excinfo[2]
+
+ @property
+ def typename(self) -> str:
+ """The type name of the exception."""
+ assert self._excinfo is not None, (
+ ".typename can only be used after the context manager exits"
+ )
+ return self.type.__name__
+
+ @property
+ def traceback(self) -> Traceback:
+ """The traceback."""
+ if self._traceback is None:
+ self._traceback = Traceback(self.tb)
+ return self._traceback
+
+ @traceback.setter
+ def traceback(self, value: Traceback) -> None:
+ self._traceback = value
+
+ def __repr__(self) -> str:
+ if self._excinfo is None:
+ return ""
+ return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
+
+ def exconly(self, tryshort: bool = False) -> str:
+ """Return the exception as a string.
+
+ This is usually a single line ": ", but
+ may also include additional lines for the exception notes, and detailed
+ information for SyntaxError's.
+
+ :param tryshort:
+ If true, and the exception is an AssertionError, strip
+ 'AssertionError: ' from the beginning.
+ """
+
+ def _get_single_subexc(
+ eg: BaseExceptionGroup[BaseException],
+ ) -> BaseException | None:
+ if len(eg.exceptions) != 1:
+ return None
+ if isinstance(e := eg.exceptions[0], BaseExceptionGroup):
+ return _get_single_subexc(e)
+ return e
+
+ if (
+ tryshort
+ and isinstance(self.value, BaseExceptionGroup)
+ and (subexc := _get_single_subexc(self.value)) is not None
+ ):
+ return f"{subexc!r} [single exception in {type(self.value).__name__}]"
+
+ lines = format_exception_only(self.value)
+ # The lines already include line separators.
+ text = "".join(lines)
+ text = text.rstrip()
+ if tryshort:
+ if text.startswith(self._striptext):
+ text = text[len(self._striptext) :]
+ return text
+
+ def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool:
+ """Return True if the exception is an instance of exc.
+
+ Consider using ``isinstance(excinfo.value, exc)`` instead.
+ """
+ return isinstance(self.value, exc)
+
+ def _getreprcrash(self) -> ReprFileLocation | None:
+ # Find last non-hidden traceback entry that led to the exception of the
+ # traceback, or None if all hidden.
+ for i in range(-1, -len(self.traceback) - 1, -1):
+ entry = self.traceback[i]
+ if not entry.ishidden(self):
+ path, lineno = entry.frame.code.raw.co_filename, entry.lineno
+ exconly = self.exconly(tryshort=True)
+ return ReprFileLocation(path, lineno + 1, exconly)
+ return None
+
+ def getrepr(
+ self,
+ showlocals: bool = False,
+ style: TracebackStyle = "long",
+ abspath: bool = False,
+ tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
+ funcargs: bool = False,
+ truncate_locals: bool = True,
+ truncate_args: bool = True,
+ chain: bool = True,
+ ) -> ReprExceptionInfo | ExceptionChainRepr:
+ """Return str()able representation of this exception info.
+
+ The formatting parameters are ineffective if ``style="native"``,
+ since in this case the native formatting is used.
+
+ :param bool showlocals:
+ Show locals per traceback entry.
+
+ :param str style:
+ long|short|line|no|native|value traceback style.
+
+ :param bool abspath:
+ If paths should be changed to absolute or left unchanged.
+
+ :param tbfilter:
+ A filter for traceback entries.
+
+ * If false, don't hide any entries.
+ * If true, hide internal entries and entries that contain a local
+ variable ``__tracebackhide__ = True``.
+ * If a callable, delegates the filtering to the callable.
+
+ :param bool funcargs:
+ Show function arguments per traceback entry.
+
+ :param bool truncate_locals:
+ Whether to show a size-limited `repr()` of locals, or a full
+ pretty-printing.
+
+ :param bool truncate_args:
+ Whether to show a size-limited truncated `repr()` of function
+ arguments, or a full pretty-printing.
+
+ :param bool chain:
+ If chained exceptions should be shown.
+
+ .. versionchanged:: 3.9
+
+ Added the ``chain`` parameter.
+ """
+ if style == "native":
+ return ReprExceptionInfo(
+ reprtraceback=ReprTracebackNative(
+ format_exception(
+ self.type,
+ self.value,
+ self.traceback[0]._rawentry if self.traceback else None,
+ )
+ ),
+ reprcrash=self._getreprcrash(),
+ )
+
+ fmt = ExceptionInfoFormatter(
+ showlocals=showlocals,
+ style=style,
+ abspath=abspath,
+ tbfilter=tbfilter,
+ funcargs=funcargs,
+ truncate_locals=truncate_locals,
+ truncate_args=truncate_args,
+ chain=chain,
+ )
+ return fmt.repr_excinfo(self)
+
+ def match(self, regexp: str | re.Pattern[str]) -> Literal[True]:
+ """Check whether the regular expression `regexp` matches the string
+ representation of the exception using :func:`python:re.search`.
+
+ If it matches `True` is returned, otherwise an `AssertionError` is raised.
+ """
+ __tracebackhide__ = True
+ value = stringify_exception(self.value)
+ msg = (
+ f"Regex pattern did not match.\n"
+ f" Expected regex: {regexp!r}\n"
+ f" Actual message: {value!r}"
+ )
+ if regexp == value:
+ msg += "\n Did you mean to `re.escape()` the regex?"
+ assert re.search(regexp, value), msg
+ # Return True to allow for "assert excinfo.match()".
+ return True
+
+ def _group_contains(
+ self,
+ exc_group: BaseExceptionGroup[BaseException],
+ expected_exception: EXCEPTION_OR_MORE,
+ match: str | re.Pattern[str] | None,
+ target_depth: int | None = None,
+ current_depth: int = 1,
+ ) -> bool:
+ """Return `True` if a `BaseExceptionGroup` contains a matching exception."""
+ if (target_depth is not None) and (current_depth > target_depth):
+ # already descended past the target depth
+ return False
+ for exc in exc_group.exceptions:
+ if isinstance(exc, BaseExceptionGroup):
+ if self._group_contains(
+ exc, expected_exception, match, target_depth, current_depth + 1
+ ):
+ return True
+ if (target_depth is not None) and (current_depth != target_depth):
+ # not at the target depth, no match
+ continue
+ if not isinstance(exc, expected_exception):
+ continue
+ if match is not None:
+ value = stringify_exception(exc)
+ if not re.search(match, value):
+ continue
+ return True
+ return False
+
+ def group_contains(
+ self,
+ expected_exception: EXCEPTION_OR_MORE,
+ *,
+ match: str | re.Pattern[str] | None = None,
+ depth: int | None = None,
+ ) -> bool:
+ """Check whether a captured exception group contains a matching exception.
+
+ :param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
+ The expected exception type, or a tuple if one of multiple possible
+ exception types are expected.
+
+ :param str | re.Pattern[str] | None match:
+ If specified, a string containing a regular expression,
+ or a regular expression object, that is tested against the string
+ representation of the exception and its `PEP-678 ` `__notes__`
+ using :func:`re.search`.
+
+ To match a literal string that may contain :ref:`special characters
+ `, the pattern can first be escaped with :func:`re.escape`.
+
+ :param Optional[int] depth:
+ If `None`, will search for a matching exception at any nesting depth.
+ If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
+ the exceptions contained within the topmost exception group).
+
+ .. versionadded:: 8.0
+
+ .. warning::
+ This helper makes it easy to check for the presence of specific exceptions,
+ but it is very bad for checking that the group does *not* contain
+ *any other exceptions*.
+ You should instead consider using :class:`pytest.RaisesGroup`
+
+ """
+ msg = "Captured exception is not an instance of `BaseExceptionGroup`"
+ assert isinstance(self.value, BaseExceptionGroup), msg
+ msg = "`depth` must be >= 1 if specified"
+ assert (depth is None) or (depth >= 1), msg
+ return self._group_contains(self.value, expected_exception, match, depth)
+
+
+# Type alias for the `tbfilter` setting:
+# bool: If True, it should be filtered using Traceback.filter()
+# callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
+TracebackFilter: TypeAlias = bool | Callable[[ExceptionInfo[BaseException]], Traceback]
+
+
+@dataclasses.dataclass
+class ExceptionInfoFormatter:
+ """Helper object to format ExceptionInfo's and individual exception parts
+ into TerminalRepr's.
+
+ See :func:`ExceptionInfo.getrepr` for parameters.
+ """
+
+ # for traceback entries
+ flow_marker: ClassVar = ">"
+ fail_marker: ClassVar = "E"
+
+ showlocals: bool = False
+ # Note: "native" is handled outside of ExceptionInfoFormatter.
+ style: TracebackStyle = "long"
+ abspath: bool = True
+ tbfilter: TracebackFilter = True
+ funcargs: bool = False
+ truncate_locals: bool = True
+ truncate_args: bool = True
+ chain: bool = True
+
+ astcache: dict[str | Path, ast.AST] = dataclasses.field(
+ default_factory=dict, init=False, repr=False
+ )
+
+ def _getindent(self, source: Source) -> int:
+ # Figure out indent for the given source.
+ try:
+ s = str(source.getstatement(len(source) - 1))
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ try:
+ s = str(source[-1])
+ except KeyboardInterrupt:
+ raise
+ except BaseException:
+ return 0
+ return 4 + (len(s) - len(s.lstrip()))
+
+ def _getentrysource(self, entry: TracebackEntry) -> Source | None:
+ source = entry.getsource(self.astcache)
+ if source is not None:
+ source = source.deindent()
+ return source
+
+ def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None:
+ if self.funcargs:
+ args = []
+ for argname, argvalue in entry.frame.getargs(var=True):
+ if self.truncate_args:
+ str_repr = saferepr(argvalue)
+ else:
+ str_repr = saferepr(argvalue, maxsize=None)
+ args.append((argname, str_repr))
+ return ReprFuncArgs(args)
+ return None
+
+ def get_source(
+ self,
+ source: Source | None,
+ line_index: int = -1,
+ excinfo: ExceptionInfo[BaseException] | None = None,
+ short: bool = False,
+ end_line_index: int | None = None,
+ colno: int | None = None,
+ end_colno: int | None = None,
+ ) -> list[str]:
+ """Return formatted and marked up source lines."""
+ lines = []
+ if source is not None and line_index < 0:
+ line_index += len(source)
+ if source is None or line_index >= len(source.lines) or line_index < 0:
+ # `line_index` could still be outside `range(len(source.lines))` if
+ # we're processing AST with pathological position attributes.
+ source = Source("???")
+ line_index = 0
+ space_prefix = " "
+ if short:
+ lines.append(space_prefix + source.lines[line_index].strip())
+ lines.extend(
+ self.get_highlight_arrows_for_line(
+ raw_line=source.raw_lines[line_index],
+ line=source.lines[line_index].strip(),
+ lineno=line_index,
+ end_lineno=end_line_index,
+ colno=colno,
+ end_colno=end_colno,
+ )
+ )
+ else:
+ for line in source.lines[:line_index]:
+ lines.append(space_prefix + line)
+ lines.append(self.flow_marker + " " + source.lines[line_index])
+ lines.extend(
+ self.get_highlight_arrows_for_line(
+ raw_line=source.raw_lines[line_index],
+ line=source.lines[line_index],
+ lineno=line_index,
+ end_lineno=end_line_index,
+ colno=colno,
+ end_colno=end_colno,
+ )
+ )
+ for line in source.lines[line_index + 1 :]:
+ lines.append(space_prefix + line)
+ if excinfo is not None:
+ indent = 4 if short else self._getindent(source)
+ lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
+ return lines
+
+ def get_highlight_arrows_for_line(
+ self,
+ line: str,
+ raw_line: str,
+ lineno: int | None,
+ end_lineno: int | None,
+ colno: int | None,
+ end_colno: int | None,
+ ) -> list[str]:
+ """Return characters highlighting a source line.
+
+ Example with colno and end_colno pointing to the bar expression:
+ "foo() + bar()"
+ returns " ^^^^^"
+ """
+ if lineno != end_lineno:
+ # Don't handle expressions that span multiple lines.
+ return []
+ if colno is None or end_colno is None:
+ # Can't do anything without column information.
+ return []
+
+ num_stripped_chars = len(raw_line) - len(line)
+
+ start_char_offset = _byte_offset_to_character_offset(raw_line, colno)
+ end_char_offset = _byte_offset_to_character_offset(raw_line, end_colno)
+ num_carets = end_char_offset - start_char_offset
+ # If the highlight would span the whole line, it is redundant, don't
+ # show it.
+ if num_carets >= len(line.strip()):
+ return []
+
+ highlights = " "
+ highlights += " " * (start_char_offset - num_stripped_chars + 1)
+ highlights += "^" * num_carets
+ return [highlights]
+
+ def get_exconly(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ indent: int = 4,
+ markall: bool = False,
+ ) -> list[str]:
+ lines = []
+ indentstr = " " * indent
+ # Get the real exception information out.
+ exlines = excinfo.exconly(tryshort=True).split("\n")
+ failindent = self.fail_marker + indentstr[1:]
+ for line in exlines:
+ lines.append(failindent + line)
+ if not markall:
+ failindent = indentstr
+ return lines
+
+ def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None:
+ if self.showlocals:
+ lines = []
+ # Variables starting with `@` are helpers injected by assertion
+ # rewriting, not user variables, so hide them.
+ keys = [loc for loc in locals if loc[0] != "@"]
+ keys.sort()
+ for name in keys:
+ value = locals[name]
+ if name == "__builtins__":
+ lines.append("__builtins__ = ")
+ else:
+ # This formatting could all be handled by the
+ # _repr() function, which is only reprlib.Repr in
+ # disguise, so is very configurable.
+ if self.truncate_locals:
+ str_repr = saferepr(value)
+ else:
+ str_repr = safeformat(value)
+ # if len(str_repr) < 70 or not isinstance(value, (list, tuple, dict)):
+ lines.append(f"{name:<10} = {str_repr}")
+ # else:
+ # self._line("%-10s =\\" % (name,))
+ # # XXX
+ # pprint.pprint(value, stream=self.excinfowriter)
+ return ReprLocals(lines)
+ return None
+
+ def repr_traceback_entry(
+ self,
+ entry: TracebackEntry | None,
+ excinfo: ExceptionInfo[BaseException] | None = None,
+ ) -> ReprEntry:
+ lines: list[str] = []
+ style = (
+ entry._repr_style
+ if entry is not None and entry._repr_style is not None
+ else self.style
+ )
+ if style in ("short", "long") and entry is not None:
+ source = self._getentrysource(entry)
+ if source is None:
+ source = Source("???")
+ line_index = 0
+ end_line_index, colno, end_colno = None, None, None
+ else:
+ line_index = entry.relline
+ end_line_index = entry.end_lineno_relative
+ colno = entry.colno
+ end_colno = entry.end_colno
+ short = style == "short"
+ reprargs = self.repr_args(entry) if not short else None
+ s = self.get_source(
+ source=source,
+ line_index=line_index,
+ excinfo=excinfo,
+ short=short,
+ end_line_index=end_line_index,
+ colno=colno,
+ end_colno=end_colno,
+ )
+ lines.extend(s)
+ if short:
+ message = f"in {entry.name}"
+ else:
+ message = (excinfo and excinfo.typename) or ""
+ entry_path = entry.path
+ path = self._makepath(entry_path)
+ reprfileloc = ReprFileLocation(path, entry.lineno + 1, message)
+ localsrepr = self.repr_locals(entry.locals)
+ return ReprEntry(lines, reprargs, localsrepr, reprfileloc, style)
+ elif style == "value":
+ if excinfo:
+ lines.extend(str(excinfo.value).split("\n"))
+ return ReprEntry(lines, None, None, None, style)
+ else:
+ if excinfo:
+ lines.extend(self.get_exconly(excinfo, indent=4))
+ return ReprEntry(lines, None, None, None, style)
+
+ def _makepath(self, path: Path | str) -> str:
+ if not self.abspath and isinstance(path, Path):
+ try:
+ np = bestrelpath(Path.cwd(), path)
+ except OSError:
+ return str(path)
+ if len(np) < len(str(path)):
+ return np
+ return str(path)
+
+ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
+ traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
+
+ if isinstance(excinfo.value, RecursionError):
+ traceback, extraline = self._truncate_recursive_traceback(traceback)
+ else:
+ extraline = None
+
+ if not traceback:
+ if extraline is None:
+ extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
+ entries = [self.repr_traceback_entry(None, excinfo)]
+ return ReprTraceback(entries, extraline, style=self.style)
+
+ last = traceback[-1]
+ if self.style == "value":
+ entries = [self.repr_traceback_entry(last, excinfo)]
+ return ReprTraceback(entries, None, style=self.style)
+
+ entries = [
+ self.repr_traceback_entry(entry, excinfo if last == entry else None)
+ for entry in traceback
+ ]
+ return ReprTraceback(entries, extraline, style=self.style)
+
+ def _truncate_recursive_traceback(
+ self, traceback: Traceback
+ ) -> tuple[Traceback, str | None]:
+ """Truncate the given recursive traceback trying to find the starting
+ point of the recursion.
+
+ The detection is done by going through each traceback entry and
+ finding the point in which the locals of the frame are equal to the
+ locals of a previous frame (see ``recursionindex()``).
+
+ Handle the situation where the recursion process might raise an
+ exception (for example comparing numpy arrays using equality raises a
+ TypeError), in which case we do our best to warn the user of the
+ error and show a limited traceback.
+ """
+ try:
+ recursionindex = traceback.recursionindex()
+ except Exception as e:
+ max_frames = 10
+ extraline: str | None = (
+ "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
+ " The following exception happened when comparing locals in the stack frame:\n"
+ f" {type(e).__name__}: {e!s}\n"
+ f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
+ )
+ # Type ignored because adding two instances of a List subtype
+ # currently incorrectly has type List instead of the subtype.
+ traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore
+ else:
+ if recursionindex is not None:
+ extraline = "!!! Recursion detected (same locals & position)"
+ traceback = traceback[: recursionindex + 1]
+ else:
+ extraline = None
+
+ return traceback, extraline
+
+ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr:
+ repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = []
+ e: BaseException | None = excinfo.value
+ excinfo_: ExceptionInfo[BaseException] | None = excinfo
+ description = None
+ seen: set[int] = set()
+ while e is not None and id(e) not in seen:
+ seen.add(id(e))
+
+ if excinfo_:
+ # Fall back to native traceback as a temporary workaround until
+ # full support for exception groups added to ExceptionInfo.
+ # See https://github.com/pytest-dev/pytest/issues/9159
+ reprtraceback: ReprTraceback | ReprTracebackNative
+ if isinstance(e, BaseExceptionGroup):
+ # don't filter any sub-exceptions since they shouldn't have any internal frames
+ traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
+ extraline = (
+ "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
+ if not traceback
+ else None
+ )
+ reprtraceback = ReprTracebackNative(
+ format_exception(
+ type(excinfo.value),
+ excinfo.value,
+ traceback[0]._rawentry if traceback else None,
+ ),
+ extraline=extraline,
+ )
+
+ else:
+ reprtraceback = self.repr_traceback(excinfo_)
+ reprcrash = excinfo_._getreprcrash()
+ else:
+ # Fallback to native repr if the exception doesn't have a traceback:
+ # ExceptionInfo objects require a full traceback to work.
+ reprtraceback = ReprTracebackNative(format_exception(type(e), e, None))
+ reprcrash = None
+ repr_chain.append((reprtraceback, reprcrash, description))
+
+ if e.__cause__ is not None and self.chain:
+ e = e.__cause__
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
+ description = "The above exception was the direct cause of the following exception:"
+ elif (
+ e.__context__ is not None and not e.__suppress_context__ and self.chain
+ ):
+ e = e.__context__
+ excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
+ description = "During handling of the above exception, another exception occurred:"
+ else:
+ e = None
+ repr_chain.reverse()
+ return ExceptionChainRepr(repr_chain)
+
+
+@dataclasses.dataclass(eq=False)
+class TerminalRepr:
+ """Base class for terminal representations -- pieces of data that display
+ themselves to a terminal."""
+
+ def __str__(self) -> str:
+ # FYI this is called from pytest-xdist's serialization of exception
+ # information.
+ io = StringIO()
+ tw = TerminalWriter(file=io)
+ self.toterminal(tw)
+ return io.getvalue().strip()
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__} instance at {id(self):0x}>"
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ raise NotImplementedError()
+
+
+@dataclasses.dataclass(eq=False)
+class ExceptionRepr(TerminalRepr):
+ """Base class for exception terminal representations.
+
+ The representation generally contains:
+ - The exception traceback (`reprtraceback`)
+ - The exception message and location (`reprcrash`)
+ - Separated, titled sections with additional data (pytest core doesn't use
+ this currently).
+ """
+
+ reprtraceback: ReprTraceback
+ reprcrash: ReprFileLocation | None
+ sections: list[tuple[str, str, str]] = dataclasses.field(
+ init=False, default_factory=list
+ )
+
+ def addsection(self, name: str, content: str, sep: str = "-") -> None:
+ self.sections.append((name, content, sep))
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for name, content, sep in self.sections:
+ tw.sep(sep, name)
+ tw.line(content)
+
+
+@dataclasses.dataclass(eq=False)
+class ExceptionChainRepr(ExceptionRepr):
+ """A chain of exceptions, separated by descriptions (e.g. "The above
+ exception was the direct cause of the following exception")."""
+
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]]
+
+ def __init__(
+ self,
+ chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]],
+ ) -> None:
+ # reprcrash and reprtraceback of the outermost (the newest) exception
+ # in the chain.
+ super().__init__(
+ reprtraceback=chain[-1][0],
+ reprcrash=chain[-1][1],
+ )
+ self.chain = chain
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for reprtraceback, reprcrash, description in self.chain:
+ reprtraceback.toterminal(tw)
+ if description is not None:
+ tw.line("")
+ tw.line(description, yellow=True)
+ super().toterminal(tw)
+
+
+@dataclasses.dataclass(eq=False)
+class ReprExceptionInfo(ExceptionRepr):
+ """A single exception with optional extra details (function arguments,
+ function locals, file location) and possible extra line and sections emitted
+ at the end."""
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ self.reprtraceback.toterminal(tw)
+ super().toterminal(tw)
+
+
+@dataclasses.dataclass(eq=False)
+class ReprTraceback(TerminalRepr):
+ """A traceback with optional extra details (function arguments, function
+ locals, file location) and possible extra line emitted at the end."""
+
+ reprentries: Sequence[ReprEntry | ReprEntryNative]
+ extraline: str | None
+ style: TracebackStyle
+
+ entrysep: ClassVar = "_ "
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # The entries might have different styles.
+ for i, entry in enumerate(self.reprentries):
+ if entry.style == "long":
+ tw.line("")
+ entry.toterminal(tw)
+ if i < len(self.reprentries) - 1:
+ next_entry = self.reprentries[i + 1]
+ if entry.style == "long" or (
+ entry.style == "short" and next_entry.style == "long"
+ ):
+ tw.sep(self.entrysep)
+
+ if self.extraline:
+ tw.line(self.extraline)
+
+
+class ReprTracebackNative(ReprTraceback):
+ """A traceback in native style.
+
+ The lines are emitted as is; uses a single entry for the entire native
+ traceback.
+ """
+
+ def __init__(self, tblines: Sequence[str], *, extraline: str | None = None) -> None:
+ self.reprentries = [ReprEntryNative(tblines)]
+ self.extraline = extraline
+ self.style = "native"
+
+
+@dataclasses.dataclass(eq=False)
+class ReprEntryNative(TerminalRepr):
+ """An entry in a traceback in native style.
+
+ Emits the lines as is. The lines are assumed to include the line separators.
+
+ [Note that we currently use a single "entry" for the entire native
+ traceback, so this is a bit misleading, but there's no point trying to parse
+ or split a native traceback.]
+ """
+
+ lines: Sequence[str]
+
+ style: ClassVar[TracebackStyle] = "native"
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ tw.write("".join(self.lines))
+
+
+@dataclasses.dataclass(eq=False)
+class ReprEntry(TerminalRepr):
+ """An entry in a traceback with possible extra details (function arguments,
+ function locals, source snippet, function's file and line)."""
+
+ lines: Sequence[str]
+ reprfuncargs: ReprFuncArgs | None
+ reprlocals: ReprLocals | None
+ reprfileloc: ReprFileLocation | None
+ style: TracebackStyle
+
+ def _write_entry_lines(self, tw: TerminalWriter) -> None:
+ """Write the source code portions of a list of traceback entries with syntax highlighting.
+
+ Usually entries are lines like these:
+
+ " x = 1"
+ "> assert x == 2"
+ "E assert 1 == 2"
+
+ This function takes care of rendering the "source" portions of it (the lines without
+ the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
+ character, as doing so might break line continuations.
+ """
+ if not self.lines:
+ return
+
+ if self.style == "value":
+ # Using tw.write instead of tw.line for testing purposes due to TWMock implementation;
+ # lines written with TWMock.line and TWMock._write_source cannot be distinguished
+ # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE
+ for line in self.lines:
+ tw.write(line)
+ tw.write("\n")
+ return
+
+ # separate indents and source lines that are not failures: we want to
+ # highlight the code but not the indentation, which may contain markers
+ # such as "> assert 0"
+ fail_marker = f"{ExceptionInfoFormatter.fail_marker} "
+ indent_size = len(fail_marker)
+ indents: list[str] = []
+ source_lines: list[str] = []
+ failure_lines: list[str] = []
+ for index, line in enumerate(self.lines):
+ is_failure_line = line.startswith(fail_marker)
+ if is_failure_line:
+ # from this point on all lines are considered part of the failure
+ failure_lines.extend(self.lines[index:])
+ break
+ else:
+ indents.append(line[:indent_size])
+ source_lines.append(line[indent_size:])
+
+ tw._write_source(source_lines, indents)
+
+ # failure lines are always completely red and bold
+ for line in failure_lines:
+ tw.line(line, bold=True, red=True)
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ if self.style == "short":
+ if self.reprfileloc:
+ self.reprfileloc.toterminal(tw)
+ self._write_entry_lines(tw)
+ if self.reprlocals:
+ self.reprlocals.toterminal(tw, indent=" " * 8)
+ return
+
+ if self.reprfuncargs:
+ self.reprfuncargs.toterminal(tw)
+
+ self._write_entry_lines(tw)
+
+ if self.reprlocals:
+ tw.line("")
+ self.reprlocals.toterminal(tw)
+ if self.reprfileloc:
+ if self.lines:
+ tw.line("")
+ self.reprfileloc.toterminal(tw)
+
+ def __str__(self) -> str:
+ return "{}\n{}\n{}".format(
+ "\n".join(self.lines), self.reprlocals, self.reprfileloc
+ )
+
+
+@dataclasses.dataclass(eq=False)
+class ReprFileLocation(TerminalRepr):
+ """A message at a file location, using the `:: `
+ format that most editors understand.
+
+ Only the first line of the message is emitted.
+ """
+
+ path: str
+ lineno: int
+ message: str
+
+ def __post_init__(self) -> None:
+ self.path = str(self.path)
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ msg = self.message
+ i = msg.find("\n")
+ if i != -1:
+ msg = msg[:i]
+ tw.write(self.path, bold=True, red=True)
+ tw.line(f":{self.lineno}: {msg}")
+
+
+@dataclasses.dataclass(eq=False)
+class ReprLocals(TerminalRepr):
+ """Function local variables (pre-formatted)."""
+
+ lines: Sequence[str]
+
+ def toterminal(self, tw: TerminalWriter, indent: str = "") -> None:
+ for line in self.lines:
+ tw.line(indent + line)
+
+
+@dataclasses.dataclass(eq=False)
+class ReprFuncArgs(TerminalRepr):
+ """Function arguments - name = value, comma separated."""
+
+ args: Sequence[tuple[str, object]]
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ if self.args:
+ linesofar = ""
+ for name, value in self.args:
+ ns = f"{name} = {value}"
+ if len(ns) + len(linesofar) + 2 > tw.fullwidth:
+ if linesofar:
+ tw.line(linesofar)
+ linesofar = ns
+ else:
+ if linesofar:
+ linesofar += ", " + ns
+ else:
+ linesofar = ns
+ if linesofar:
+ tw.line(linesofar)
+ tw.line("")
+
+
+def getfslineno(obj: object) -> tuple[str | Path, int]:
+ """Return source location (path, lineno) for the given object.
+
+ If the source cannot be determined return ("", -1).
+
+ The line number is 0-based.
+ """
+ # xxx let decorators etc specify a sane ordering
+ # NOTE: this used to be done in _pytest.compat.getfslineno, initially added
+ # in 6ec13a2b9. It ("place_as") appears to be something very custom.
+ obj = get_real_func(obj)
+ if hasattr(obj, "place_as"):
+ obj = obj.place_as
+
+ try:
+ code = Code.from_function(obj)
+ except TypeError:
+ try:
+ fn = inspect.getsourcefile(obj) or inspect.getfile(obj) # type: ignore[arg-type]
+ except TypeError:
+ return "", -1
+
+ fspath = (fn and absolutepath(fn)) or ""
+ lineno = -1
+ if fspath:
+ try:
+ _, lineno = findsource(obj)
+ except OSError:
+ pass
+ return fspath, lineno
+
+ return code.path, code.firstlineno
+
+
+def _byte_offset_to_character_offset(str, offset):
+ """Converts a byte based offset in a string to a code-point."""
+ as_utf8 = str.encode("utf-8")
+ return len(as_utf8[:offset].decode("utf-8", errors="replace"))
+
+
+# Relative paths that we use to filter traceback entries from appearing to the user;
+# see filter_traceback.
+# note: if we need to add more paths than what we have now we should probably use a list
+# for better maintenance.
+
+_PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
+# pluggy is either a package or a single module depending on the version
+if _PLUGGY_DIR.name == "__init__.py":
+ _PLUGGY_DIR = _PLUGGY_DIR.parent
+_PYTEST_DIR = Path(_pytest.__file__).parent
+
+
+def filter_traceback(entry: TracebackEntry) -> bool:
+ """Return True if a TracebackEntry instance should be included in tracebacks.
+
+ We hide traceback entries of:
+
+ * dynamically generated code (no code to show up for it);
+ * internal traceback from pytest or its internal libraries, py and pluggy.
+ """
+ # entry.path might sometimes return a str object when the entry
+ # points to dynamically generated code.
+ # See https://bitbucket.org/pytest-dev/py/issues/71.
+ raw_filename = entry.frame.code.raw.co_filename
+ is_generated = "<" in raw_filename and ">" in raw_filename
+ if is_generated:
+ return False
+
+ # entry.path might point to a non-existing file, in which case it will
+ # also return a str object. See #1133.
+ p = Path(entry.path)
+
+ parents = p.parents
+ if _PLUGGY_DIR in parents:
+ return False
+ if _PYTEST_DIR in parents:
+ return False
+
+ return True
+
+
+def filter_excinfo_traceback(
+ tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
+) -> Traceback:
+ """Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
+ if callable(tbfilter):
+ return tbfilter(excinfo)
+ elif tbfilter:
+ return excinfo.traceback.filter(excinfo)
+ else:
+ return excinfo.traceback
diff --git a/venv/lib/python3.11/site-packages/_pytest/_code/source.py b/venv/lib/python3.11/site-packages/_pytest/_code/source.py
new file mode 100644
index 000000000..cbadf6679
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_code/source.py
@@ -0,0 +1,228 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+import ast
+from bisect import bisect_right
+from collections.abc import Iterable
+from collections.abc import Iterator
+import inspect
+import textwrap
+import tokenize
+import types
+from typing import overload
+import warnings
+
+
+class Source:
+ """An immutable object holding a source code fragment.
+
+ When using Source(...), the source lines are deindented.
+ """
+
+ def __init__(self, obj: object = None) -> None:
+ if not obj:
+ self.lines: list[str] = []
+ self.raw_lines: list[str] = []
+ elif isinstance(obj, Source):
+ self.lines = obj.lines
+ self.raw_lines = obj.raw_lines
+ elif isinstance(obj, tuple | list):
+ self.lines = deindent(x.rstrip("\n") for x in obj)
+ self.raw_lines = list(x.rstrip("\n") for x in obj)
+ elif isinstance(obj, str):
+ self.lines = deindent(obj.split("\n"))
+ self.raw_lines = obj.split("\n")
+ else:
+ try:
+ rawcode = getrawcode(obj)
+ src = inspect.getsource(rawcode)
+ except TypeError:
+ src = inspect.getsource(obj) # type: ignore[arg-type]
+ self.lines = deindent(src.split("\n"))
+ self.raw_lines = src.split("\n")
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Source):
+ return NotImplemented
+ return self.lines == other.lines
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ @overload
+ def __getitem__(self, key: int) -> str: ...
+
+ @overload
+ def __getitem__(self, key: slice) -> Source: ...
+
+ def __getitem__(self, key: int | slice) -> str | Source:
+ if isinstance(key, int):
+ return self.lines[key]
+ else:
+ if key.step not in (None, 1):
+ raise IndexError("cannot slice a Source with a step")
+ newsource = Source()
+ newsource.lines = self.lines[key.start : key.stop]
+ newsource.raw_lines = self.raw_lines[key.start : key.stop]
+ return newsource
+
+ def __iter__(self) -> Iterator[str]:
+ return iter(self.lines)
+
+ def __len__(self) -> int:
+ return len(self.lines)
+
+ def strip(self) -> Source:
+ """Return new Source object with trailing and leading blank lines removed."""
+ start, end = 0, len(self)
+ while start < end and not self.lines[start].strip():
+ start += 1
+ while end > start and not self.lines[end - 1].strip():
+ end -= 1
+ source = Source()
+ source.raw_lines = self.raw_lines
+ source.lines[:] = self.lines[start:end]
+ return source
+
+ def indent(self, indent: str = " " * 4) -> Source:
+ """Return a copy of the source object with all lines indented by the
+ given indent-string."""
+ newsource = Source()
+ newsource.raw_lines = self.raw_lines
+ newsource.lines = [(indent + line) for line in self.lines]
+ return newsource
+
+ def getstatement(self, lineno: int) -> Source:
+ """Return Source statement which contains the given linenumber
+ (counted from 0)."""
+ start, end = self.getstatementrange(lineno)
+ return self[start:end]
+
+ def getstatementrange(self, lineno: int) -> tuple[int, int]:
+ """Return (start, end) tuple which spans the minimal statement region
+ which containing the given lineno."""
+ if not (0 <= lineno < len(self)):
+ raise IndexError("lineno out of range")
+ _ast, start, end = getstatementrange_ast(lineno, self)
+ return start, end
+
+ def deindent(self) -> Source:
+ """Return a new Source object deindented."""
+ newsource = Source()
+ newsource.lines[:] = deindent(self.lines)
+ newsource.raw_lines = self.raw_lines
+ return newsource
+
+ def __str__(self) -> str:
+ return "\n".join(self.lines)
+
+
+#
+# helper functions
+#
+
+
+def findsource(obj) -> tuple[Source | None, int]:
+ try:
+ sourcelines, lineno = inspect.findsource(obj)
+ except Exception:
+ return None, -1
+ source = Source()
+ source.lines = [line.rstrip() for line in sourcelines]
+ source.raw_lines = sourcelines
+ return source, lineno
+
+
+def getrawcode(obj: object, trycall: bool = True) -> types.CodeType:
+ """Return code object for given function."""
+ try:
+ return obj.__code__ # type: ignore[attr-defined,no-any-return]
+ except AttributeError:
+ pass
+ if trycall:
+ call = getattr(obj, "__call__", None)
+ if call and not isinstance(obj, type):
+ return getrawcode(call, trycall=False)
+ raise TypeError(f"could not get code object for {obj!r}")
+
+
+def deindent(lines: Iterable[str]) -> list[str]:
+ return textwrap.dedent("\n".join(lines)).splitlines()
+
+
+def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]:
+ # Flatten all statements and except handlers into one lineno-list.
+ # AST's line numbers start indexing at 1.
+ values: list[int] = []
+ for x in ast.walk(node):
+ if isinstance(x, ast.stmt | ast.ExceptHandler):
+ # The lineno points to the class/def, so need to include the decorators.
+ if isinstance(x, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
+ for d in x.decorator_list:
+ values.append(d.lineno - 1)
+ values.append(x.lineno - 1)
+ for name in ("finalbody", "orelse"):
+ val: list[ast.stmt] | None = getattr(x, name, None)
+ if val:
+ # Treat the finally/orelse part as its own statement.
+ values.append(val[0].lineno - 1 - 1)
+ values.sort()
+ insert_index = bisect_right(values, lineno)
+ if insert_index == 0:
+ return 0, None
+ start = values[insert_index - 1]
+ if insert_index >= len(values):
+ end = None
+ else:
+ end = values[insert_index]
+ return start, end
+
+
+def getstatementrange_ast(
+ lineno: int,
+ source: Source,
+ assertion: bool = False,
+ astnode: ast.AST | None = None,
+) -> tuple[ast.AST, int, int]:
+ if astnode is None:
+ content = str(source)
+ # See #4260:
+ # Don't produce duplicate warnings when compiling source to find AST.
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ astnode = ast.parse(content, "source", "exec")
+
+ start, end = get_statement_startend2(lineno, astnode)
+ # We need to correct the end:
+ # - ast-parsing strips comments
+ # - there might be empty lines
+ # - we might have lesser indented code blocks at the end
+ if end is None:
+ end = len(source.lines)
+
+ if end > start + 1:
+ # Make sure we don't span differently indented code blocks
+ # by using the BlockFinder helper used which inspect.getsource() uses itself.
+ block_finder = inspect.BlockFinder()
+ # If we start with an indented line, put blockfinder to "started" mode.
+ block_finder.started = (
+ bool(source.lines[start]) and source.lines[start][0].isspace()
+ )
+ it = ((x + "\n") for x in source.lines[start:end])
+ try:
+ for tok in tokenize.generate_tokens(lambda: next(it)):
+ block_finder.tokeneater(*tok)
+ except (inspect.EndOfBlock, IndentationError):
+ end = block_finder.last + start
+ except Exception:
+ pass
+
+ # The end might still point to a comment or empty line, correct it.
+ end = min(end, len(source.lines))
+ while end:
+ line = source.lines[end - 1].lstrip()
+ if line.startswith("#") or not line:
+ end -= 1
+ else:
+ break
+ return astnode, start, end
diff --git a/venv/lib/python3.11/site-packages/_pytest/_io/__init__.py b/venv/lib/python3.11/site-packages/_pytest/_io/__init__.py
new file mode 100644
index 000000000..b0155b18b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_io/__init__.py
@@ -0,0 +1,10 @@
+from __future__ import annotations
+
+from .terminalwriter import get_terminal_width
+from .terminalwriter import TerminalWriter
+
+
+__all__ = [
+ "TerminalWriter",
+ "get_terminal_width",
+]
diff --git a/venv/lib/python3.11/site-packages/_pytest/_io/pprint.py b/venv/lib/python3.11/site-packages/_pytest/_io/pprint.py
new file mode 100644
index 000000000..ec41b449d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_io/pprint.py
@@ -0,0 +1,673 @@
+# mypy: allow-untyped-defs
+# This module was imported from the cpython standard library
+# (https://github.com/python/cpython/) at commit
+# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
+#
+#
+# Original Author: Fred L. Drake, Jr.
+# fdrake@acm.org
+#
+# This is a simple little module I wrote to make life easier. I didn't
+# see anything quite like it in the library, though I may have overlooked
+# something. I wrote this when I was trying to read some heavily nested
+# tuples with fairly non-descriptive content. This is modeled very much
+# after Lisp/Scheme - style pretty-printing of lists. If you find it
+# useful, thank small children who sleep at night.
+from __future__ import annotations
+
+import collections as _collections
+from collections.abc import Callable
+from collections.abc import Iterator
+import dataclasses as _dataclasses
+from io import StringIO as _StringIO
+import re
+import types as _types
+from typing import Any
+from typing import IO
+
+
+class _safe_key:
+ """Helper function for key functions when sorting unorderable objects.
+
+ The wrapped-object will fallback to a Py2.x style comparison for
+ unorderable types (sorting first comparing the type name and then by
+ the obj ids). Does not work recursively, so dict.items() must have
+ _safe_key applied to both the key and the value.
+
+ """
+
+ __slots__ = ["obj"]
+
+ def __init__(self, obj):
+ self.obj = obj
+
+ def __lt__(self, other):
+ try:
+ return self.obj < other.obj
+ except TypeError:
+ return (str(type(self.obj)), id(self.obj)) < (
+ str(type(other.obj)),
+ id(other.obj),
+ )
+
+
+def _safe_tuple(t):
+ """Helper function for comparing 2-tuples"""
+ return _safe_key(t[0]), _safe_key(t[1])
+
+
+class PrettyPrinter:
+ def __init__(
+ self,
+ indent: int = 4,
+ width: int = 80,
+ depth: int | None = None,
+ ) -> None:
+ """Handle pretty printing operations onto a stream using a set of
+ configured parameters.
+
+ indent
+ Number of spaces to indent for each level of nesting.
+
+ width
+ Attempted maximum number of columns in the output.
+
+ depth
+ The maximum depth to print out nested structures.
+
+ """
+ if indent < 0:
+ raise ValueError("indent must be >= 0")
+ if depth is not None and depth <= 0:
+ raise ValueError("depth must be > 0")
+ if not width:
+ raise ValueError("width must be != 0")
+ self._depth = depth
+ self._indent_per_level = indent
+ self._width = width
+
+ def pformat(self, object: Any) -> str:
+ sio = _StringIO()
+ self._format(object, sio, 0, 0, set(), 0)
+ return sio.getvalue()
+
+ def _format(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ objid = id(object)
+ if objid in context:
+ stream.write(_recursion(object))
+ return
+
+ p = self._dispatch.get(type(object).__repr__, None)
+ if p is not None:
+ context.add(objid)
+ p(self, object, stream, indent, allowance, context, level + 1)
+ context.remove(objid)
+ elif (
+ _dataclasses.is_dataclass(object)
+ and not isinstance(object, type)
+ and object.__dataclass_params__.repr # type:ignore[attr-defined]
+ and
+ # Check dataclass has generated repr method.
+ hasattr(object.__repr__, "__wrapped__")
+ and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
+ ):
+ context.add(objid)
+ self._pprint_dataclass(
+ object, stream, indent, allowance, context, level + 1
+ )
+ context.remove(objid)
+ else:
+ stream.write(self._repr(object, context, level))
+
+ def _pprint_dataclass(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ cls_name = object.__class__.__name__
+ items = [
+ (f.name, getattr(object, f.name))
+ for f in _dataclasses.fields(object)
+ if f.repr
+ ]
+ stream.write(cls_name + "(")
+ self._format_namespace_items(items, stream, indent, allowance, context, level)
+ stream.write(")")
+
+ _dispatch: dict[
+ Callable[..., str],
+ Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None],
+ ] = {}
+
+ def _pprint_dict(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ write = stream.write
+ write("{")
+ items = object.items()
+ self._format_dict_items(items, stream, indent, allowance, context, level)
+ write("}")
+
+ _dispatch[dict.__repr__] = _pprint_dict
+
+ def _pprint_ordered_dict(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ if not len(object):
+ stream.write(repr(object))
+ return
+ cls = object.__class__
+ stream.write(cls.__name__ + "(")
+ self._pprint_dict(object, stream, indent, allowance, context, level)
+ stream.write(")")
+
+ _dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
+
+ def _pprint_list(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ stream.write("[")
+ self._format_items(object, stream, indent, allowance, context, level)
+ stream.write("]")
+
+ _dispatch[list.__repr__] = _pprint_list
+
+ def _pprint_tuple(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ stream.write("(")
+ self._format_items(object, stream, indent, allowance, context, level)
+ stream.write(")")
+
+ _dispatch[tuple.__repr__] = _pprint_tuple
+
+ def _pprint_set(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ if not len(object):
+ stream.write(repr(object))
+ return
+ typ = object.__class__
+ if typ is set:
+ stream.write("{")
+ endchar = "}"
+ else:
+ stream.write(typ.__name__ + "({")
+ endchar = "})"
+ object = sorted(object, key=_safe_key)
+ self._format_items(object, stream, indent, allowance, context, level)
+ stream.write(endchar)
+
+ _dispatch[set.__repr__] = _pprint_set
+ _dispatch[frozenset.__repr__] = _pprint_set
+
+ def _pprint_str(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ write = stream.write
+ if not len(object):
+ write(repr(object))
+ return
+ chunks = []
+ lines = object.splitlines(True)
+ if level == 1:
+ indent += 1
+ allowance += 1
+ max_width1 = max_width = self._width - indent
+ for i, line in enumerate(lines):
+ rep = repr(line)
+ if i == len(lines) - 1:
+ max_width1 -= allowance
+ if len(rep) <= max_width1:
+ chunks.append(rep)
+ else:
+ # A list of alternating (non-space, space) strings
+ parts = re.findall(r"\S*\s*", line)
+ assert parts
+ assert not parts[-1]
+ parts.pop() # drop empty last part
+ max_width2 = max_width
+ current = ""
+ for j, part in enumerate(parts):
+ candidate = current + part
+ if j == len(parts) - 1 and i == len(lines) - 1:
+ max_width2 -= allowance
+ if len(repr(candidate)) > max_width2:
+ if current:
+ chunks.append(repr(current))
+ current = part
+ else:
+ current = candidate
+ if current:
+ chunks.append(repr(current))
+ if len(chunks) == 1:
+ write(rep)
+ return
+ if level == 1:
+ write("(")
+ for i, rep in enumerate(chunks):
+ if i > 0:
+ write("\n" + " " * indent)
+ write(rep)
+ if level == 1:
+ write(")")
+
+ _dispatch[str.__repr__] = _pprint_str
+
+ def _pprint_bytes(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ write = stream.write
+ if len(object) <= 4:
+ write(repr(object))
+ return
+ parens = level == 1
+ if parens:
+ indent += 1
+ allowance += 1
+ write("(")
+ delim = ""
+ for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
+ write(delim)
+ write(rep)
+ if not delim:
+ delim = "\n" + " " * indent
+ if parens:
+ write(")")
+
+ _dispatch[bytes.__repr__] = _pprint_bytes
+
+ def _pprint_bytearray(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ write = stream.write
+ write("bytearray(")
+ self._pprint_bytes(
+ bytes(object), stream, indent + 10, allowance + 1, context, level + 1
+ )
+ write(")")
+
+ _dispatch[bytearray.__repr__] = _pprint_bytearray
+
+ def _pprint_mappingproxy(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ stream.write("mappingproxy(")
+ self._format(object.copy(), stream, indent, allowance, context, level)
+ stream.write(")")
+
+ _dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
+
+ def _pprint_simplenamespace(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ if type(object) is _types.SimpleNamespace:
+ # The SimpleNamespace repr is "namespace" instead of the class
+ # name, so we do the same here. For subclasses; use the class name.
+ cls_name = "namespace"
+ else:
+ cls_name = object.__class__.__name__
+ items = object.__dict__.items()
+ stream.write(cls_name + "(")
+ self._format_namespace_items(items, stream, indent, allowance, context, level)
+ stream.write(")")
+
+ _dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
+
+ def _format_dict_items(
+ self,
+ items: list[tuple[Any, Any]],
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ if not items:
+ return
+
+ write = stream.write
+ item_indent = indent + self._indent_per_level
+ delimnl = "\n" + " " * item_indent
+ for key, ent in items:
+ write(delimnl)
+ write(self._repr(key, context, level))
+ write(": ")
+ self._format(ent, stream, item_indent, 1, context, level)
+ write(",")
+
+ write("\n" + " " * indent)
+
+ def _format_namespace_items(
+ self,
+ items: list[tuple[Any, Any]],
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ if not items:
+ return
+
+ write = stream.write
+ item_indent = indent + self._indent_per_level
+ delimnl = "\n" + " " * item_indent
+ for key, ent in items:
+ write(delimnl)
+ write(key)
+ write("=")
+ if id(ent) in context:
+ # Special-case representation of recursion to match standard
+ # recursive dataclass repr.
+ write("...")
+ else:
+ self._format(
+ ent,
+ stream,
+ item_indent + len(key) + 1,
+ 1,
+ context,
+ level,
+ )
+
+ write(",")
+
+ write("\n" + " " * indent)
+
+ def _format_items(
+ self,
+ items: list[Any],
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ if not items:
+ return
+
+ write = stream.write
+ item_indent = indent + self._indent_per_level
+ delimnl = "\n" + " " * item_indent
+
+ for item in items:
+ write(delimnl)
+ self._format(item, stream, item_indent, 1, context, level)
+ write(",")
+
+ write("\n" + " " * indent)
+
+ def _repr(self, object: Any, context: set[int], level: int) -> str:
+ return self._safe_repr(object, context.copy(), self._depth, level)
+
+ def _pprint_default_dict(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ rdf = self._repr(object.default_factory, context, level)
+ stream.write(f"{object.__class__.__name__}({rdf}, ")
+ self._pprint_dict(object, stream, indent, allowance, context, level)
+ stream.write(")")
+
+ _dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
+
+ def _pprint_counter(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ stream.write(object.__class__.__name__ + "(")
+
+ if object:
+ stream.write("{")
+ items = object.most_common()
+ self._format_dict_items(items, stream, indent, allowance, context, level)
+ stream.write("}")
+
+ stream.write(")")
+
+ _dispatch[_collections.Counter.__repr__] = _pprint_counter
+
+ def _pprint_chain_map(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
+ stream.write(repr(object))
+ return
+
+ stream.write(object.__class__.__name__ + "(")
+ self._format_items(object.maps, stream, indent, allowance, context, level)
+ stream.write(")")
+
+ _dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
+
+ def _pprint_deque(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ stream.write(object.__class__.__name__ + "(")
+ if object.maxlen is not None:
+ stream.write(f"maxlen={object.maxlen}, ")
+ stream.write("[")
+
+ self._format_items(object, stream, indent, allowance + 1, context, level)
+ stream.write("])")
+
+ _dispatch[_collections.deque.__repr__] = _pprint_deque
+
+ def _pprint_user_dict(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ self._format(object.data, stream, indent, allowance, context, level - 1)
+
+ _dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
+
+ def _pprint_user_list(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ self._format(object.data, stream, indent, allowance, context, level - 1)
+
+ _dispatch[_collections.UserList.__repr__] = _pprint_user_list
+
+ def _pprint_user_string(
+ self,
+ object: Any,
+ stream: IO[str],
+ indent: int,
+ allowance: int,
+ context: set[int],
+ level: int,
+ ) -> None:
+ self._format(object.data, stream, indent, allowance, context, level - 1)
+
+ _dispatch[_collections.UserString.__repr__] = _pprint_user_string
+
+ def _safe_repr(
+ self, object: Any, context: set[int], maxlevels: int | None, level: int
+ ) -> str:
+ typ = type(object)
+ if typ in _builtin_scalars:
+ return repr(object)
+
+ r = getattr(typ, "__repr__", None)
+
+ if issubclass(typ, dict) and r is dict.__repr__:
+ if not object:
+ return "{}"
+ objid = id(object)
+ if maxlevels and level >= maxlevels:
+ return "{...}"
+ if objid in context:
+ return _recursion(object)
+ context.add(objid)
+ components: list[str] = []
+ append = components.append
+ level += 1
+ for k, v in object.items():
+ krepr = self._safe_repr(k, context, maxlevels, level)
+ vrepr = self._safe_repr(v, context, maxlevels, level)
+ append(f"{krepr}: {vrepr}")
+ context.remove(objid)
+ return "{{{}}}".format(", ".join(components))
+
+ if (issubclass(typ, list) and r is list.__repr__) or (
+ issubclass(typ, tuple) and r is tuple.__repr__
+ ):
+ if issubclass(typ, list):
+ if not object:
+ return "[]"
+ format = "[%s]"
+ elif len(object) == 1:
+ format = "(%s,)"
+ else:
+ if not object:
+ return "()"
+ format = "(%s)"
+ objid = id(object)
+ if maxlevels and level >= maxlevels:
+ return format % "..."
+ if objid in context:
+ return _recursion(object)
+ context.add(objid)
+ components = []
+ append = components.append
+ level += 1
+ for o in object:
+ orepr = self._safe_repr(o, context, maxlevels, level)
+ append(orepr)
+ context.remove(objid)
+ return format % ", ".join(components)
+
+ return repr(object)
+
+
+_builtin_scalars = frozenset(
+ {str, bytes, bytearray, float, complex, bool, type(None), int}
+)
+
+
+def _recursion(object: Any) -> str:
+ return f""
+
+
+def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
+ current = b""
+ last = len(object) // 4 * 4
+ for i in range(0, len(object), 4):
+ part = object[i : i + 4]
+ candidate = current + part
+ if i == last:
+ width -= allowance
+ if len(repr(candidate)) > width:
+ if current:
+ yield repr(current)
+ current = part
+ else:
+ current = candidate
+ if current:
+ yield repr(current)
diff --git a/venv/lib/python3.11/site-packages/_pytest/_io/saferepr.py b/venv/lib/python3.11/site-packages/_pytest/_io/saferepr.py
new file mode 100644
index 000000000..3f5c956d9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_io/saferepr.py
@@ -0,0 +1,155 @@
+from __future__ import annotations
+
+from itertools import islice
+import pprint
+import reprlib
+
+
+def _try_repr_or_str(obj: object) -> str:
+ try:
+ return repr(obj)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException:
+ return f'{type(obj).__name__}("{obj}")'
+
+
+def _format_repr_exception(exc: BaseException, obj: object) -> str:
+ try:
+ exc_info = _try_repr_or_str(exc)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as inner_exc:
+ exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})"
+ return (
+ f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
+ )
+
+
+def _ellipsize(s: str, maxsize: int) -> str:
+ if len(s) > maxsize:
+ i = max(0, (maxsize - 3) // 2)
+ j = max(0, maxsize - 3 - i)
+ return s[:i] + "..." + s[len(s) - j :]
+ return s
+
+
+class SafeRepr(reprlib.Repr):
+ """
+ repr.Repr that limits the resulting size of repr() and includes
+ information on exceptions raised during the call.
+ """
+
+ def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None:
+ """
+ :param maxsize:
+ If not None, will truncate the resulting repr to that specific size, using ellipsis
+ somewhere in the middle to hide the extra text.
+ If None, will not impose any size limits on the returning repr.
+ """
+ super().__init__()
+ # ``maxstring`` is used by the superclass, and needs to be an int; using a
+ # very large number in case maxsize is None, meaning we want to disable
+ # truncation.
+ self.maxstring = maxsize if maxsize is not None else 1_000_000_000
+ self.maxsize = maxsize
+ self.use_ascii = use_ascii
+
+ def repr(self, x: object) -> str:
+ try:
+ if self.use_ascii:
+ s = ascii(x)
+ else:
+ s = super().repr(x)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as exc:
+ s = _format_repr_exception(exc, x)
+ if self.maxsize is not None:
+ s = _ellipsize(s, self.maxsize)
+ return s
+
+ def repr_instance(self, x: object, level: int) -> str:
+ try:
+ s = repr(x)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except BaseException as exc:
+ s = _format_repr_exception(exc, x)
+ if self.maxsize is not None:
+ s = _ellipsize(s, self.maxsize)
+
+ return s
+
+ def repr_dict(self, x: dict[object, object], level: int) -> str:
+ """Represent a dict while preserving its insertion order.
+
+ Differs from ``reprlib.Repr.repr_dict`` by iterating directly over ``x``
+ rather than using the stdlib's sorting helper.
+ """
+ fillvalue = "..."
+ n = len(x)
+ if n == 0:
+ return "{}"
+ if level <= 0:
+ return "{" + fillvalue + "}"
+ newlevel = level - 1
+ repr1 = self.repr1
+ pieces = []
+ for key in islice(x, self.maxdict):
+ keyrepr = repr1(key, newlevel)
+ valrepr = repr1(x[key], newlevel)
+ pieces.append(f"{keyrepr}: {valrepr}")
+ if n > self.maxdict:
+ pieces.append(fillvalue)
+ return "{" + ", ".join(pieces) + "}"
+
+
+def safeformat(obj: object) -> str:
+ """Return a pretty printed string for the given object.
+
+ Failing __repr__ functions of user instances will be represented
+ with a short exception info.
+ """
+ try:
+ return pprint.pformat(obj)
+ except Exception as exc:
+ return _format_repr_exception(exc, obj)
+
+
+# Maximum size of overall repr of objects to display during assertion errors.
+DEFAULT_REPR_MAX_SIZE = 240
+
+
+def saferepr(
+ obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
+) -> str:
+ """Return a size-limited safe repr-string for the given object.
+
+ Failing __repr__ functions of user instances will be represented
+ with a short exception info and 'saferepr' generally takes
+ care to never raise exceptions itself.
+
+ This function is a wrapper around the Repr/reprlib functionality of the
+ stdlib.
+ """
+ return SafeRepr(maxsize, use_ascii).repr(obj)
+
+
+def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
+ """Return an unlimited-size safe repr-string for the given object.
+
+ As with saferepr, failing __repr__ functions of user instances
+ will be represented with a short exception info.
+
+ This function is a wrapper around simple repr.
+
+ Note: a cleaner solution would be to alter ``saferepr``this way
+ when maxsize=None, but that might affect some other code.
+ """
+ try:
+ if use_ascii:
+ return ascii(obj)
+ return repr(obj)
+ except Exception as exc:
+ return _format_repr_exception(exc, obj)
diff --git a/venv/lib/python3.11/site-packages/_pytest/_io/terminalwriter.py b/venv/lib/python3.11/site-packages/_pytest/_io/terminalwriter.py
new file mode 100644
index 000000000..9191b4eda
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_io/terminalwriter.py
@@ -0,0 +1,258 @@
+"""Helper functions for writing to terminals and files."""
+
+from __future__ import annotations
+
+from collections.abc import Sequence
+import os
+import shutil
+import sys
+from typing import final
+from typing import Literal
+from typing import TextIO
+
+import pygments
+from pygments.formatters.terminal import TerminalFormatter
+from pygments.lexer import Lexer
+from pygments.lexers.diff import DiffLexer
+from pygments.lexers.python import PythonLexer
+
+from ..compat import assert_never
+from .wcwidth import wcswidth
+
+
+# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
+
+
+def get_terminal_width() -> int:
+ width, _ = shutil.get_terminal_size(fallback=(80, 24))
+
+ # The Windows get_terminal_size may be bogus, let's sanify a bit.
+ if width < 40:
+ width = 80
+
+ return width
+
+
+def should_do_markup(file: TextIO) -> bool:
+ if os.environ.get("PY_COLORS") == "1":
+ return True
+ if os.environ.get("PY_COLORS") == "0":
+ return False
+ if os.environ.get("NO_COLOR"):
+ return False
+ if os.environ.get("FORCE_COLOR"):
+ return True
+ return (
+ hasattr(file, "isatty") and file.isatty() and os.environ.get("TERM") != "dumb"
+ )
+
+
+@final
+class TerminalWriter:
+ _esctable = dict(
+ black=30,
+ red=31,
+ green=32,
+ yellow=33,
+ blue=34,
+ purple=35,
+ cyan=36,
+ white=37,
+ Black=40,
+ Red=41,
+ Green=42,
+ Yellow=43,
+ Blue=44,
+ Purple=45,
+ Cyan=46,
+ White=47,
+ bold=1,
+ light=2,
+ blink=5,
+ invert=7,
+ )
+
+ def __init__(self, file: TextIO | None = None) -> None:
+ if file is None:
+ file = sys.stdout
+ if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32":
+ try:
+ import colorama
+ except ImportError:
+ pass
+ else:
+ file = colorama.AnsiToWin32(file).stream
+ assert file is not None
+ self._file = file
+ self.hasmarkup = should_do_markup(file)
+ self._current_line = ""
+ self._terminal_width: int | None = None
+ self.code_highlight = True
+
+ @property
+ def fullwidth(self) -> int:
+ if self._terminal_width is not None:
+ return self._terminal_width
+ return get_terminal_width()
+
+ @fullwidth.setter
+ def fullwidth(self, value: int) -> None:
+ self._terminal_width = value
+
+ @property
+ def width_of_current_line(self) -> int:
+ """Return an estimate of the width so far in the current line."""
+ return wcswidth(self._current_line)
+
+ def markup(self, text: str, **markup: bool) -> str:
+ for name in markup:
+ if name not in self._esctable:
+ raise ValueError(f"unknown markup: {name!r}")
+ if self.hasmarkup:
+ esc = [self._esctable[name] for name, on in markup.items() if on]
+ if esc:
+ text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m"
+ return text
+
+ def sep(
+ self,
+ sepchar: str,
+ title: str | None = None,
+ fullwidth: int | None = None,
+ **markup: bool,
+ ) -> None:
+ if fullwidth is None:
+ fullwidth = self.fullwidth
+ # The goal is to have the line be as long as possible
+ # under the condition that len(line) <= fullwidth.
+ if sys.platform == "win32":
+ # If we print in the last column on windows we are on a
+ # new line but there is no way to verify/neutralize this
+ # (we may not know the exact line width).
+ # So let's be defensive to avoid empty lines in the output.
+ fullwidth -= 1
+ if title is not None:
+ # we want 2 + 2*len(fill) + len(title) <= fullwidth
+ # i.e. 2 + 2*len(sepchar)*N + len(title) <= fullwidth
+ # 2*len(sepchar)*N <= fullwidth - len(title) - 2
+ # N <= (fullwidth - len(title) - 2) // (2*len(sepchar))
+ N = max((fullwidth - len(title) - 2) // (2 * len(sepchar)), 1)
+ fill = sepchar * N
+ line = f"{fill} {title} {fill}"
+ else:
+ # we want len(sepchar)*N <= fullwidth
+ # i.e. N <= fullwidth // len(sepchar)
+ line = sepchar * (fullwidth // len(sepchar))
+ # In some situations there is room for an extra sepchar at the right,
+ # in particular if we consider that with a sepchar like "_ " the
+ # trailing space is not important at the end of the line.
+ if len(line) + len(sepchar.rstrip()) <= fullwidth:
+ line += sepchar.rstrip()
+
+ self.line(line, **markup)
+
+ def write(self, msg: str, *, flush: bool = False, **markup: bool) -> None:
+ if msg:
+ current_line = msg.rsplit("\n", 1)[-1]
+ if "\n" in msg:
+ self._current_line = current_line
+ else:
+ self._current_line += current_line
+
+ msg = self.markup(msg, **markup)
+
+ self.write_raw(msg, flush=flush)
+
+ def write_raw(self, msg: str, *, flush: bool = False) -> None:
+ try:
+ self._file.write(msg)
+ except UnicodeEncodeError:
+ # Some environments don't support printing general Unicode
+ # strings, due to misconfiguration or otherwise; in that case,
+ # print the string escaped to ASCII.
+ # When the Unicode situation improves we should consider
+ # letting the error propagate instead of masking it (see #7475
+ # for one brief attempt).
+ msg = msg.encode("unicode-escape").decode("ascii")
+ self._file.write(msg)
+
+ if flush:
+ self.flush()
+
+ def line(self, s: str = "", **markup: bool) -> None:
+ self.write(s, **markup)
+ self.write("\n")
+
+ def flush(self) -> None:
+ self._file.flush()
+
+ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> None:
+ """Write lines of source code possibly highlighted.
+
+ Keeping this private for now because the API is clunky. We should discuss how
+ to evolve the terminal writer so we can have more precise color support, for example
+ being able to write part of a line in one color and the rest in another, and so on.
+ """
+ if indents and len(indents) != len(lines):
+ raise ValueError(
+ f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
+ )
+ if not indents:
+ indents = [""] * len(lines)
+ source = "\n".join(lines)
+ new_lines = self._highlight(source).splitlines()
+ # Would be better to strict=True but that fails some CI jobs.
+ for indent, new_line in zip(indents, new_lines, strict=False):
+ self.line(indent + new_line)
+
+ def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer:
+ if lexer == "python":
+ return PythonLexer()
+ elif lexer == "diff":
+ return DiffLexer()
+ else:
+ assert_never(lexer)
+
+ def _get_pygments_formatter(self) -> TerminalFormatter:
+ from _pytest.config.exceptions import UsageError
+
+ theme = os.getenv("PYTEST_THEME")
+ theme_mode = os.getenv("PYTEST_THEME_MODE", "dark")
+
+ try:
+ return TerminalFormatter(bg=theme_mode, style=theme)
+ except pygments.util.ClassNotFound as e:
+ raise UsageError(
+ f"PYTEST_THEME environment variable has an invalid value: '{theme}'. "
+ "Hint: See available pygments styles with `pygmentize -L styles`."
+ ) from e
+ except pygments.util.OptionError as e:
+ raise UsageError(
+ f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. "
+ "The allowed values are 'dark' (default) and 'light'."
+ ) from e
+
+ def _highlight(
+ self, source: str, lexer: Literal["diff", "python"] = "python"
+ ) -> str:
+ """Highlight the given source if we have markup support."""
+ if not source or not self.hasmarkup or not self.code_highlight:
+ return source
+
+ pygments_lexer = self._get_pygments_lexer(lexer)
+ pygments_formatter = self._get_pygments_formatter()
+
+ highlighted: str = pygments.highlight(
+ source, pygments_lexer, pygments_formatter
+ )
+ # pygments terminal formatter may add a newline when there wasn't one.
+ # We don't want this, remove.
+ if highlighted[-1] == "\n" and source[-1] != "\n":
+ highlighted = highlighted[:-1]
+
+ # Some lexers will not set the initial color explicitly
+ # which may lead to the previous color being propagated to the
+ # start of the expression, so reset first.
+ highlighted = "\x1b[0m" + highlighted
+
+ return highlighted
diff --git a/venv/lib/python3.11/site-packages/_pytest/_io/wcwidth.py b/venv/lib/python3.11/site-packages/_pytest/_io/wcwidth.py
new file mode 100644
index 000000000..23886ff15
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_io/wcwidth.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from functools import lru_cache
+import unicodedata
+
+
+@lru_cache(100)
+def wcwidth(c: str) -> int:
+ """Determine how many columns are needed to display a character in a terminal.
+
+ Returns -1 if the character is not printable.
+ Returns 0, 1 or 2 for other characters.
+ """
+ o = ord(c)
+
+ # ASCII fast path.
+ if 0x20 <= o < 0x07F:
+ return 1
+
+ # Some Cf/Zp/Zl characters which should be zero-width.
+ if (
+ o == 0x0000
+ or 0x200B <= o <= 0x200F
+ or 0x2028 <= o <= 0x202E
+ or 0x2060 <= o <= 0x2063
+ ):
+ return 0
+
+ category = unicodedata.category(c)
+
+ # Control characters.
+ if category == "Cc":
+ return -1
+
+ # Combining characters with zero width.
+ if category in ("Me", "Mn"):
+ return 0
+
+ # Full/Wide east asian characters.
+ if unicodedata.east_asian_width(c) in ("F", "W"):
+ return 2
+
+ return 1
+
+
+def wcswidth(s: str) -> int:
+ """Determine how many columns are needed to display a string in a terminal.
+
+ Returns -1 if the string contains non-printable characters.
+ """
+ width = 0
+ for c in unicodedata.normalize("NFC", s):
+ wc = wcwidth(c)
+ if wc < 0:
+ return -1
+ width += wc
+ return width
diff --git a/venv/lib/python3.11/site-packages/_pytest/_py/__init__.py b/venv/lib/python3.11/site-packages/_pytest/_py/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/_pytest/_py/error.py b/venv/lib/python3.11/site-packages/_pytest/_py/error.py
new file mode 100644
index 000000000..dace23764
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_py/error.py
@@ -0,0 +1,119 @@
+"""create errno-specific classes for IO or os calls."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+import errno
+import os
+import sys
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+
+if TYPE_CHECKING:
+ from typing_extensions import ParamSpec
+
+ P = ParamSpec("P")
+
+R = TypeVar("R")
+
+
+class Error(EnvironmentError):
+ def __repr__(self) -> str:
+ return "{}.{} {!r}: {} ".format(
+ self.__class__.__module__,
+ self.__class__.__name__,
+ self.__class__.__doc__,
+ " ".join(map(str, self.args)),
+ # repr(self.args)
+ )
+
+ def __str__(self) -> str:
+ s = "[{}]: {}".format(
+ self.__class__.__doc__,
+ " ".join(map(str, self.args)),
+ )
+ return s
+
+
+_winerrnomap = {
+ 2: errno.ENOENT,
+ 3: errno.ENOENT,
+ 17: errno.EEXIST,
+ 18: errno.EXDEV,
+ 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable
+ 22: errno.ENOTDIR,
+ 20: errno.ENOTDIR,
+ 267: errno.ENOTDIR,
+ 5: errno.EACCES, # anything better?
+}
+
+
+class ErrorMaker:
+ """lazily provides Exception classes for each possible POSIX errno
+ (as defined per the 'errno' module). All such instances
+ subclass EnvironmentError.
+ """
+
+ _errno2class: dict[int, type[Error]] = {}
+
+ def __getattr__(self, name: str) -> type[Error]:
+ if name[0] == "_":
+ raise AttributeError(name)
+ eno = getattr(errno, name)
+ cls = self._geterrnoclass(eno)
+ setattr(self, name, cls)
+ return cls
+
+ def _geterrnoclass(self, eno: int) -> type[Error]:
+ try:
+ return self._errno2class[eno]
+ except KeyError:
+ clsname = errno.errorcode.get(eno, f"UnknownErrno{eno}")
+ errorcls = type(
+ clsname,
+ (Error,),
+ {"__module__": "py.error", "__doc__": os.strerror(eno)},
+ )
+ self._errno2class[eno] = errorcls
+ return errorcls
+
+ def checked_call(
+ self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
+ ) -> R:
+ """Call a function and raise an errno-exception if applicable."""
+ __tracebackhide__ = True
+ try:
+ return func(*args, **kwargs)
+ except Error:
+ raise
+ except OSError as value:
+ if not hasattr(value, "errno"):
+ raise
+ if sys.platform == "win32":
+ try:
+ # error: Invalid index type "Optional[int]" for "dict[int, int]"; expected type "int" [index]
+ # OK to ignore because we catch the KeyError below.
+ cls = self._geterrnoclass(_winerrnomap[value.errno]) # type:ignore[index]
+ except KeyError:
+ raise value
+ else:
+ # we are not on Windows, or we got a proper OSError
+ if value.errno is None:
+ cls = type(
+ "UnknownErrnoNone",
+ (Error,),
+ {"__module__": "py.error", "__doc__": None},
+ )
+ else:
+ cls = self._geterrnoclass(value.errno)
+
+ raise cls(f"{func.__name__}{args!r}")
+
+
+_error_maker = ErrorMaker()
+checked_call = _error_maker.checked_call
+
+
+def __getattr__(attr: str) -> type[Error]:
+ return getattr(_error_maker, attr) # type: ignore[no-any-return]
diff --git a/venv/lib/python3.11/site-packages/_pytest/_py/path.py b/venv/lib/python3.11/site-packages/_pytest/_py/path.py
new file mode 100644
index 000000000..998a78199
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_py/path.py
@@ -0,0 +1,1475 @@
+# mypy: allow-untyped-defs
+"""local path implementation."""
+
+from __future__ import annotations
+
+import atexit
+from collections.abc import Callable
+from contextlib import contextmanager
+import fnmatch
+import importlib.util
+import io
+import os
+from os.path import abspath
+from os.path import dirname
+from os.path import exists
+from os.path import isabs
+from os.path import isdir
+from os.path import isfile
+from os.path import islink
+from os.path import normpath
+import posixpath
+from stat import S_ISDIR
+from stat import S_ISLNK
+from stat import S_ISREG
+import sys
+from typing import Any
+from typing import cast
+from typing import Literal
+from typing import overload
+from typing import TYPE_CHECKING
+import uuid
+import warnings
+
+from . import error
+
+
+# Moved from local.py.
+iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
+
+
+class Checkers:
+ _depend_on_existence = "exists", "link", "dir", "file"
+
+ def __init__(self, path):
+ self.path = path
+
+ def dotfile(self):
+ return self.path.basename.startswith(".")
+
+ def ext(self, arg):
+ if not arg.startswith("."):
+ arg = "." + arg
+ return self.path.ext == arg
+
+ def basename(self, arg):
+ return self.path.basename == arg
+
+ def basestarts(self, arg):
+ return self.path.basename.startswith(arg)
+
+ def relto(self, arg):
+ return self.path.relto(arg)
+
+ def fnmatch(self, arg):
+ return self.path.fnmatch(arg)
+
+ def endswith(self, arg):
+ return str(self.path).endswith(arg)
+
+ def _evaluate(self, kw):
+ from .._code.source import getrawcode
+
+ for name, value in kw.items():
+ invert = False
+ meth = None
+ try:
+ meth = getattr(self, name)
+ except AttributeError:
+ if name[:3] == "not":
+ invert = True
+ try:
+ meth = getattr(self, name[3:])
+ except AttributeError:
+ pass
+ if meth is None:
+ raise TypeError(f"no {name!r} checker available for {self.path!r}")
+ try:
+ if getrawcode(meth).co_argcount > 1:
+ if (not meth(value)) ^ invert:
+ return False
+ else:
+ if bool(value) ^ bool(meth()) ^ invert:
+ return False
+ except (error.ENOENT, error.ENOTDIR, error.EBUSY):
+ # EBUSY feels not entirely correct,
+ # but its kind of necessary since ENOMEDIUM
+ # is not accessible in python
+ for name in self._depend_on_existence:
+ if name in kw:
+ if kw.get(name):
+ return False
+ name = "not" + name
+ if name in kw:
+ if not kw.get(name):
+ return False
+ return True
+
+ _statcache: Stat
+
+ def _stat(self) -> Stat:
+ try:
+ return self._statcache
+ except AttributeError:
+ try:
+ self._statcache = self.path.stat()
+ except error.ELOOP:
+ self._statcache = self.path.lstat()
+ return self._statcache
+
+ def dir(self):
+ return S_ISDIR(self._stat().mode)
+
+ def file(self):
+ return S_ISREG(self._stat().mode)
+
+ def exists(self):
+ return self._stat()
+
+ def link(self):
+ st = self.path.lstat()
+ return S_ISLNK(st.mode)
+
+
+class NeverRaised(Exception):
+ pass
+
+
+class Visitor:
+ def __init__(self, fil, rec, ignore, bf, sort):
+ if isinstance(fil, (str, bytes)):
+ fil = FNMatcher(fil)
+ if isinstance(rec, str):
+ self.rec: Callable[[LocalPath], bool] = FNMatcher(rec)
+ elif not hasattr(rec, "__call__") and rec:
+ self.rec = lambda path: True
+ else:
+ self.rec = rec
+ self.fil = fil
+ self.ignore = ignore
+ self.breadthfirst = bf
+ self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
+
+ def gen(self, path):
+ try:
+ entries = path.listdir()
+ except self.ignore:
+ return
+ rec = self.rec
+ dirs = self.optsort(
+ [p for p in entries if p.check(dir=1) and (rec is None or rec(p))]
+ )
+ if not self.breadthfirst:
+ for subdir in dirs:
+ yield from self.gen(subdir)
+ for p in self.optsort(entries):
+ if self.fil is None or self.fil(p):
+ yield p
+ if self.breadthfirst:
+ for subdir in dirs:
+ yield from self.gen(subdir)
+
+
+class FNMatcher:
+ def __init__(self, pattern):
+ self.pattern = pattern
+
+ def __call__(self, path):
+ pattern = self.pattern
+
+ if (
+ pattern.find(path.sep) == -1
+ and iswin32
+ and pattern.find(posixpath.sep) != -1
+ ):
+ # Running on Windows, the pattern has no Windows path separators,
+ # and the pattern has one or more Posix path separators. Replace
+ # the Posix path separators with the Windows path separator.
+ pattern = pattern.replace(posixpath.sep, path.sep)
+
+ if pattern.find(path.sep) == -1:
+ name = path.basename
+ else:
+ name = str(path) # path.strpath # XXX svn?
+ if not os.path.isabs(pattern):
+ pattern = "*" + path.sep + pattern
+ return fnmatch.fnmatch(name, pattern)
+
+
+def map_as_list(func, iter):
+ return list(map(func, iter))
+
+
+class Stat:
+ if TYPE_CHECKING:
+
+ @property
+ def size(self) -> int: ...
+
+ @property
+ def mtime(self) -> float: ...
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._osstatresult, "st_" + name)
+
+ def __init__(self, path, osstatresult):
+ self.path = path
+ self._osstatresult = osstatresult
+
+ @property
+ def owner(self):
+ if iswin32:
+ raise NotImplementedError("XXX win32")
+ import pwd
+
+ entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
+ return entry[0]
+
+ @property
+ def group(self):
+ """Return group name of file."""
+ if iswin32:
+ raise NotImplementedError("XXX win32")
+ import grp
+
+ entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
+ return entry[0]
+
+ def isdir(self):
+ return S_ISDIR(self._osstatresult.st_mode)
+
+ def isfile(self):
+ return S_ISREG(self._osstatresult.st_mode)
+
+ def islink(self):
+ self.path.lstat()
+ return S_ISLNK(self._osstatresult.st_mode)
+
+
+def getuserid(user):
+ import pwd
+
+ if not isinstance(user, int):
+ user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
+ return user
+
+
+def getgroupid(group):
+ import grp
+
+ if not isinstance(group, int):
+ group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
+ return group
+
+
+class LocalPath:
+ """Object oriented interface to os.path and other local filesystem
+ related information.
+ """
+
+ class ImportMismatchError(ImportError):
+ """raised on pyimport() if there is a mismatch of __file__'s"""
+
+ sep = os.sep
+
+ def __init__(self, path=None, expanduser=False):
+ """Initialize and return a local Path instance.
+
+ Path can be relative to the current directory.
+ If path is None it defaults to the current working directory.
+ If expanduser is True, tilde-expansion is performed.
+ Note that Path instances always carry an absolute path.
+ Note also that passing in a local path object will simply return
+ the exact same path object. Use new() to get a new copy.
+ """
+ if path is None:
+ self.strpath = error.checked_call(os.getcwd)
+ else:
+ try:
+ path = os.fspath(path)
+ except TypeError:
+ raise ValueError(
+ "can only pass None, Path instances "
+ "or non-empty strings to LocalPath"
+ )
+ if expanduser:
+ path = os.path.expanduser(path)
+ self.strpath = abspath(path)
+
+ if sys.platform != "win32":
+
+ def chown(self, user, group, rec=0):
+ """Change ownership to the given user and group.
+ user and group may be specified by a number or
+ by a name. if rec is True change ownership
+ recursively.
+ """
+ uid = getuserid(user)
+ gid = getgroupid(group)
+ if rec:
+ for x in self.visit(rec=lambda x: x.check(link=0)):
+ if x.check(link=0):
+ error.checked_call(os.chown, str(x), uid, gid)
+ error.checked_call(os.chown, str(self), uid, gid)
+
+ def readlink(self) -> str:
+ """Return value of a symbolic link."""
+ # https://github.com/python/mypy/issues/12278
+ return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
+
+ def mklinkto(self, oldname):
+ """Posix style hard link to another name."""
+ error.checked_call(os.link, str(oldname), str(self))
+
+ def mksymlinkto(self, value, absolute=1):
+ """Create a symbolic link with the given value (pointing to another name)."""
+ if absolute:
+ error.checked_call(os.symlink, str(value), self.strpath)
+ else:
+ base = self.common(value)
+ # with posix local paths '/' is always a common base
+ relsource = self.__class__(value).relto(base)
+ reldest = self.relto(base)
+ n = reldest.count(self.sep)
+ target = self.sep.join(("..",) * n + (relsource,))
+ error.checked_call(os.symlink, target, self.strpath)
+
+ def __div__(self, other):
+ return self.join(os.fspath(other))
+
+ __truediv__ = __div__ # py3k
+
+ @property
+ def basename(self):
+ """Basename part of path."""
+ return self._getbyspec("basename")[0]
+
+ @property
+ def dirname(self):
+ """Dirname part of path."""
+ return self._getbyspec("dirname")[0]
+
+ @property
+ def purebasename(self):
+ """Pure base name of the path."""
+ return self._getbyspec("purebasename")[0]
+
+ @property
+ def ext(self):
+ """Extension of the path (including the '.')."""
+ return self._getbyspec("ext")[0]
+
+ def read_binary(self):
+ """Read and return a bytestring from reading the path."""
+ with self.open("rb") as f:
+ return f.read()
+
+ def read_text(self, encoding):
+ """Read and return a Unicode string from reading the path."""
+ with self.open("r", encoding=encoding) as f:
+ return f.read()
+
+ def read(self, mode="r"):
+ """Read and return a bytestring from reading the path."""
+ with self.open(mode) as f:
+ return f.read()
+
+ def readlines(self, cr=1):
+ """Read and return a list of lines from the path. if cr is False, the
+ newline will be removed from the end of each line."""
+ mode = "r"
+
+ if not cr:
+ content = self.read(mode)
+ return content.split("\n")
+ else:
+ f = self.open(mode)
+ try:
+ return f.readlines()
+ finally:
+ f.close()
+
+ def load(self):
+ """(deprecated) return object unpickled from self.read()"""
+ f = self.open("rb")
+ try:
+ import pickle
+
+ return error.checked_call(pickle.load, f)
+ finally:
+ f.close()
+
+ def move(self, target):
+ """Move this path to target."""
+ if target.relto(self):
+ raise error.EINVAL(target, "cannot move path into a subdirectory of itself")
+ try:
+ self.rename(target)
+ except error.EXDEV: # invalid cross-device link
+ self.copy(target)
+ self.remove()
+
+ def fnmatch(self, pattern):
+ """Return true if the basename/fullname matches the glob-'pattern'.
+
+ valid pattern characters::
+
+ * matches everything
+ ? matches any single character
+ [seq] matches any character in seq
+ [!seq] matches any char not in seq
+
+ If the pattern contains a path-separator then the full path
+ is used for pattern matching and a '*' is prepended to the
+ pattern.
+
+ if the pattern doesn't contain a path-separator the pattern
+ is only matched against the basename.
+ """
+ return FNMatcher(pattern)(self)
+
+ def relto(self, relpath):
+ """Return a string which is the relative part of the path
+ to the given 'relpath'.
+ """
+ if not isinstance(relpath, str | LocalPath):
+ raise TypeError(f"{relpath!r}: not a string or path object")
+ strrelpath = str(relpath)
+ if strrelpath and strrelpath[-1] != self.sep:
+ strrelpath += self.sep
+ # assert strrelpath[-1] == self.sep
+ # assert strrelpath[-2] != self.sep
+ strself = self.strpath
+ if sys.platform == "win32" or getattr(os, "_name", None) == "nt":
+ if os.path.normcase(strself).startswith(os.path.normcase(strrelpath)):
+ return strself[len(strrelpath) :]
+ elif strself.startswith(strrelpath):
+ return strself[len(strrelpath) :]
+ return ""
+
+ def ensure_dir(self, *args):
+ """Ensure the path joined with args is a directory."""
+ return self.ensure(*args, dir=True)
+
+ def bestrelpath(self, dest):
+ """Return a string which is a relative path from self
+ (assumed to be a directory) to dest such that
+ self.join(bestrelpath) == dest and if not such
+ path can be determined return dest.
+ """
+ try:
+ if self == dest:
+ return os.curdir
+ base = self.common(dest)
+ if not base: # can be the case on windows
+ return str(dest)
+ self2base = self.relto(base)
+ reldest = dest.relto(base)
+ if self2base:
+ n = self2base.count(self.sep) + 1
+ else:
+ n = 0
+ lst = [os.pardir] * n
+ if reldest:
+ lst.append(reldest)
+ target = dest.sep.join(lst)
+ return target
+ except AttributeError:
+ return str(dest)
+
+ def exists(self):
+ return self.check()
+
+ def isdir(self):
+ return self.check(dir=1)
+
+ def isfile(self):
+ return self.check(file=1)
+
+ def parts(self, reverse=False):
+ """Return a root-first list of all ancestor directories
+ plus the path itself.
+ """
+ current = self
+ lst = [self]
+ while 1:
+ last = current
+ current = current.dirpath()
+ if last == current:
+ break
+ lst.append(current)
+ if not reverse:
+ lst.reverse()
+ return lst
+
+ def common(self, other):
+ """Return the common part shared with the other path
+ or None if there is no common part.
+ """
+ last = None
+ for x, y in zip(self.parts(), other.parts()):
+ if x != y:
+ return last
+ last = x
+ return last
+
+ def __add__(self, other):
+ """Return new path object with 'other' added to the basename"""
+ return self.new(basename=self.basename + str(other))
+
+ def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False):
+ """Yields all paths below the current one
+
+ fil is a filter (glob pattern or callable), if not matching the
+ path will not be yielded, defaulting to None (everything is
+ returned)
+
+ rec is a filter (glob pattern or callable) that controls whether
+ a node is descended, defaulting to None
+
+ ignore is an Exception class that is ignoredwhen calling dirlist()
+ on any of the paths (by default, all exceptions are reported)
+
+ bf if True will cause a breadthfirst search instead of the
+ default depthfirst. Default: False
+
+ sort if True will sort entries within each directory level.
+ """
+ yield from Visitor(fil, rec, ignore, bf, sort).gen(self)
+
+ def _sortlist(self, res, sort):
+ if sort:
+ if hasattr(sort, "__call__"):
+ warnings.warn(
+ DeprecationWarning(
+ "listdir(sort=callable) is deprecated and breaks on python3"
+ ),
+ stacklevel=3,
+ )
+ res.sort(sort)
+ else:
+ res.sort()
+
+ def __fspath__(self):
+ return self.strpath
+
+ def __hash__(self):
+ s = self.strpath
+ if iswin32:
+ s = s.lower()
+ return hash(s)
+
+ def __eq__(self, other):
+ s1 = os.fspath(self)
+ try:
+ s2 = os.fspath(other)
+ except TypeError:
+ return False
+ if iswin32:
+ s1 = s1.lower()
+ try:
+ s2 = s2.lower()
+ except AttributeError:
+ return False
+ return s1 == s2
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __lt__(self, other):
+ return os.fspath(self) < os.fspath(other)
+
+ def __gt__(self, other):
+ return os.fspath(self) > os.fspath(other)
+
+ def samefile(self, other):
+ """Return True if 'other' references the same file as 'self'."""
+ other = os.fspath(other)
+ if not isabs(other):
+ other = abspath(other)
+ if self == other:
+ return True
+ if not hasattr(os.path, "samefile"):
+ return False
+ return error.checked_call(os.path.samefile, self.strpath, other)
+
+ def remove(self, rec=1, ignore_errors=False):
+ """Remove a file or directory (or a directory tree if rec=1).
+ if ignore_errors is True, errors while removing directories will
+ be ignored.
+ """
+ if self.check(dir=1, link=0):
+ if rec:
+ # force remove of readonly files on windows
+ if iswin32:
+ self.chmod(0o700, rec=1)
+ import shutil
+
+ error.checked_call(
+ shutil.rmtree, self.strpath, ignore_errors=ignore_errors
+ )
+ else:
+ error.checked_call(os.rmdir, self.strpath)
+ else:
+ if iswin32:
+ self.chmod(0o700)
+ error.checked_call(os.remove, self.strpath)
+
+ def computehash(self, hashtype="md5", chunksize=524288):
+ """Return hexdigest of hashvalue for this file."""
+ try:
+ try:
+ import hashlib as mod
+ except ImportError:
+ if hashtype == "sha1":
+ hashtype = "sha"
+ mod = __import__(hashtype)
+ hash = getattr(mod, hashtype)()
+ except (AttributeError, ImportError):
+ raise ValueError(f"Don't know how to compute {hashtype!r} hash")
+ f = self.open("rb")
+ try:
+ while 1:
+ buf = f.read(chunksize)
+ if not buf:
+ return hash.hexdigest()
+ hash.update(buf)
+ finally:
+ f.close()
+
+ def new(self, **kw):
+ """Create a modified version of this path.
+ the following keyword arguments modify various path parts::
+
+ a:/some/path/to/a/file.ext
+ xx drive
+ xxxxxxxxxxxxxxxxx dirname
+ xxxxxxxx basename
+ xxxx purebasename
+ xxx ext
+ """
+ obj = object.__new__(self.__class__)
+ if not kw:
+ obj.strpath = self.strpath
+ return obj
+ drive, dirname, _basename, purebasename, ext = self._getbyspec(
+ "drive,dirname,basename,purebasename,ext"
+ )
+ if "basename" in kw:
+ if "purebasename" in kw or "ext" in kw:
+ raise ValueError(f"invalid specification {kw!r}")
+ else:
+ pb = kw.setdefault("purebasename", purebasename)
+ try:
+ ext = kw["ext"]
+ except KeyError:
+ pass
+ else:
+ if ext and not ext.startswith("."):
+ ext = "." + ext
+ kw["basename"] = pb + ext
+
+ if "dirname" in kw and not kw["dirname"]:
+ kw["dirname"] = drive
+ else:
+ kw.setdefault("dirname", dirname)
+ kw.setdefault("sep", self.sep)
+ obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
+ return obj
+
+ def _getbyspec(self, spec: str) -> list[str]:
+ """See new for what 'spec' can be."""
+ res = []
+ parts = self.strpath.split(self.sep)
+
+ args = filter(None, spec.split(","))
+ for name in args:
+ if name == "drive":
+ res.append(parts[0])
+ elif name == "dirname":
+ res.append(self.sep.join(parts[:-1]))
+ else:
+ basename = parts[-1]
+ if name == "basename":
+ res.append(basename)
+ else:
+ i = basename.rfind(".")
+ if i == -1:
+ purebasename, ext = basename, ""
+ else:
+ purebasename, ext = basename[:i], basename[i:]
+ if name == "purebasename":
+ res.append(purebasename)
+ elif name == "ext":
+ res.append(ext)
+ else:
+ raise ValueError(f"invalid part specification {name!r}")
+ return res
+
+ def dirpath(self, *args, **kwargs):
+ """Return the directory path joined with any given path arguments."""
+ if not kwargs:
+ path = object.__new__(self.__class__)
+ path.strpath = dirname(self.strpath)
+ if args:
+ path = path.join(*args)
+ return path
+ return self.new(basename="").join(*args, **kwargs)
+
+ def join(self, *args: os.PathLike[str], abs: bool = False) -> LocalPath:
+ """Return a new path by appending all 'args' as path
+ components. if abs=1 is used restart from root if any
+ of the args is an absolute path.
+ """
+ sep = self.sep
+ strargs = [os.fspath(arg) for arg in args]
+ strpath = self.strpath
+ if abs:
+ newargs: list[str] = []
+ for arg in reversed(strargs):
+ if isabs(arg):
+ strpath = arg
+ strargs = newargs
+ break
+ newargs.insert(0, arg)
+ # special case for when we have e.g. strpath == "/"
+ actual_sep = "" if strpath.endswith(sep) else sep
+ for arg in strargs:
+ arg = arg.strip(sep)
+ if iswin32:
+ # allow unix style paths even on windows.
+ arg = arg.strip("/")
+ arg = arg.replace("/", sep)
+ strpath = strpath + actual_sep + arg
+ actual_sep = sep
+ obj = object.__new__(self.__class__)
+ obj.strpath = normpath(strpath)
+ return obj
+
+ def open(self, mode="r", ensure=False, encoding=None):
+ """Return an opened file with the given mode.
+
+ If ensure is True, create parent directories if needed.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ if encoding:
+ return error.checked_call(
+ io.open,
+ self.strpath,
+ mode,
+ encoding=encoding,
+ )
+ return error.checked_call(open, self.strpath, mode)
+
+ def _fastjoin(self, name):
+ child = object.__new__(self.__class__)
+ child.strpath = self.strpath + self.sep + name
+ return child
+
+ def islink(self):
+ return islink(self.strpath)
+
+ def check(self, **kw):
+ """Check a path for existence and properties.
+
+ Without arguments, return True if the path exists, otherwise False.
+
+ valid checkers::
+
+ file = 1 # is a file
+ file = 0 # is not a file (may not even exist)
+ dir = 1 # is a dir
+ link = 1 # is a link
+ exists = 1 # exists
+
+ You can specify multiple checker definitions, for example::
+
+ path.check(file=1, link=1) # a link pointing to a file
+ """
+ if not kw:
+ return exists(self.strpath)
+ if len(kw) == 1:
+ if "dir" in kw:
+ return not kw["dir"] ^ isdir(self.strpath)
+ if "file" in kw:
+ return not kw["file"] ^ isfile(self.strpath)
+ if not kw:
+ kw = {"exists": 1}
+ return Checkers(self)._evaluate(kw)
+
+ _patternchars = set("*?[" + os.sep)
+
+ def listdir(self, fil=None, sort=None):
+ """List directory contents, possibly filter by the given fil func
+ and possibly sorted.
+ """
+ if fil is None and sort is None:
+ names = error.checked_call(os.listdir, self.strpath)
+ return map_as_list(self._fastjoin, names)
+ if isinstance(fil, str):
+ if not self._patternchars.intersection(fil):
+ child = self._fastjoin(fil)
+ if exists(child.strpath):
+ return [child]
+ return []
+ fil = FNMatcher(fil)
+ names = error.checked_call(os.listdir, self.strpath)
+ res = []
+ for name in names:
+ child = self._fastjoin(name)
+ if fil is None or fil(child):
+ res.append(child)
+ self._sortlist(res, sort)
+ return res
+
+ def size(self) -> int:
+ """Return size of the underlying file object"""
+ return self.stat().size
+
+ def mtime(self) -> float:
+ """Return last modification time of the path."""
+ return self.stat().mtime
+
+ def copy(self, target, mode=False, stat=False):
+ """Copy path to target.
+
+ If mode is True, will copy permission from path to target.
+ If stat is True, copy permission, last modification
+ time, last access time, and flags from path to target.
+ """
+ if self.check(file=1):
+ if target.check(dir=1):
+ target = target.join(self.basename)
+ assert self != target
+ copychunked(self, target)
+ if mode:
+ copymode(self.strpath, target.strpath)
+ if stat:
+ copystat(self, target)
+ else:
+
+ def rec(p):
+ return p.check(link=0)
+
+ for x in self.visit(rec=rec):
+ relpath = x.relto(self)
+ newx = target.join(relpath)
+ newx.dirpath().ensure(dir=1)
+ if x.check(link=1):
+ newx.mksymlinkto(x.readlink())
+ continue
+ elif x.check(file=1):
+ copychunked(x, newx)
+ elif x.check(dir=1):
+ newx.ensure(dir=1)
+ if mode:
+ copymode(x.strpath, newx.strpath)
+ if stat:
+ copystat(x, newx)
+
+ def rename(self, target):
+ """Rename this path to target."""
+ target = os.fspath(target)
+ return error.checked_call(os.rename, self.strpath, target)
+
+ def dump(self, obj, bin=1):
+ """Pickle object into path location"""
+ f = self.open("wb")
+ import pickle
+
+ try:
+ error.checked_call(pickle.dump, obj, f, bin)
+ finally:
+ f.close()
+
+ def mkdir(self, *args):
+ """Create & return the directory joined with args."""
+ p = self.join(*args)
+ error.checked_call(os.mkdir, os.fspath(p))
+ return p
+
+ def write_binary(self, data, ensure=False):
+ """Write binary data into path. If ensure is True create
+ missing parent directories.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ with self.open("wb") as f:
+ f.write(data)
+
+ def write_text(self, data, encoding, ensure=False):
+ """Write text data into path using the specified encoding.
+ If ensure is True create missing parent directories.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ with self.open("w", encoding=encoding) as f:
+ f.write(data)
+
+ def write(self, data, mode="w", ensure=False):
+ """Write data into path. If ensure is True create
+ missing parent directories.
+ """
+ if ensure:
+ self.dirpath().ensure(dir=1)
+ if "b" in mode:
+ if not isinstance(data, bytes):
+ raise ValueError("can only process bytes")
+ else:
+ if not isinstance(data, str):
+ if not isinstance(data, bytes):
+ data = str(data)
+ else:
+ data = data.decode(sys.getdefaultencoding())
+ f = self.open(mode)
+ try:
+ f.write(data)
+ finally:
+ f.close()
+
+ def _ensuredirs(self):
+ parent = self.dirpath()
+ if parent == self:
+ return self
+ if parent.check(dir=0):
+ parent._ensuredirs()
+ if self.check(dir=0):
+ try:
+ self.mkdir()
+ except error.EEXIST:
+ # race condition: file/dir created by another thread/process.
+ # complain if it is not a dir
+ if self.check(dir=0):
+ raise
+ return self
+
+ def ensure(self, *args, **kwargs):
+ """Ensure that an args-joined path exists (by default as
+ a file). if you specify a keyword argument 'dir=True'
+ then the path is forced to be a directory path.
+ """
+ p = self.join(*args)
+ if kwargs.get("dir", 0):
+ return p._ensuredirs()
+ else:
+ p.dirpath()._ensuredirs()
+ if not p.check(file=1):
+ p.open("wb").close()
+ return p
+
+ @overload
+ def stat(self, raising: Literal[True] = ...) -> Stat: ...
+
+ @overload
+ def stat(self, raising: Literal[False]) -> Stat | None: ...
+
+ def stat(self, raising: bool = True) -> Stat | None:
+ """Return an os.stat() tuple."""
+ if raising:
+ return Stat(self, error.checked_call(os.stat, self.strpath))
+ try:
+ return Stat(self, os.stat(self.strpath))
+ except KeyboardInterrupt:
+ raise
+ except Exception:
+ return None
+
+ def lstat(self) -> Stat:
+ """Return an os.lstat() tuple."""
+ return Stat(self, error.checked_call(os.lstat, self.strpath))
+
+ def setmtime(self, mtime=None):
+ """Set modification time for the given path. if 'mtime' is None
+ (the default) then the file's mtime is set to current time.
+
+ Note that the resolution for 'mtime' is platform dependent.
+ """
+ if mtime is None:
+ return error.checked_call(os.utime, self.strpath, mtime)
+ try:
+ return error.checked_call(os.utime, self.strpath, (-1, mtime))
+ except error.EINVAL:
+ return error.checked_call(os.utime, self.strpath, (self.atime(), mtime))
+
+ def chdir(self):
+ """Change directory to self and return old current directory"""
+ try:
+ old = self.__class__()
+ except error.ENOENT:
+ old = None
+ error.checked_call(os.chdir, self.strpath)
+ return old
+
+ @contextmanager
+ def as_cwd(self):
+ """
+ Return a context manager, which changes to the path's dir during the
+ managed "with" context.
+ On __enter__ it returns the old dir, which might be ``None``.
+ """
+ old = self.chdir()
+ try:
+ yield old
+ finally:
+ if old is not None:
+ old.chdir()
+
+ def realpath(self):
+ """Return a new path which contains no symbolic links."""
+ return self.__class__(os.path.realpath(self.strpath))
+
+ def atime(self):
+ """Return last access time of the path."""
+ return self.stat().atime
+
+ def __repr__(self):
+ return f"local({self.strpath!r})"
+
+ def __str__(self):
+ """Return string representation of the Path."""
+ return self.strpath
+
+ def chmod(self, mode, rec=0):
+ """Change permissions to the given mode. If mode is an
+ integer it directly encodes the os-specific modes.
+ if rec is True perform recursively.
+ """
+ if not isinstance(mode, int):
+ raise TypeError(f"mode {mode!r} must be an integer")
+ if rec:
+ for x in self.visit(rec=rec):
+ error.checked_call(os.chmod, str(x), mode)
+ error.checked_call(os.chmod, self.strpath, mode)
+
+ def pypkgpath(self):
+ """Return the Python package path by looking for the last
+ directory upwards which still contains an __init__.py.
+ Return None if a pkgpath cannot be determined.
+ """
+ pkgpath = None
+ for parent in self.parts(reverse=True):
+ if parent.isdir():
+ if not parent.join("__init__.py").exists():
+ break
+ if not isimportable(parent.basename):
+ break
+ pkgpath = parent
+ return pkgpath
+
+ def _ensuresyspath(self, ensuremode, path):
+ if ensuremode:
+ s = str(path)
+ if ensuremode == "append":
+ if s not in sys.path:
+ sys.path.append(s)
+ else:
+ if s != sys.path[0]:
+ sys.path.insert(0, s)
+
+ def pyimport(self, modname=None, ensuresyspath=True):
+ """Return path as an imported python module.
+
+ If modname is None, look for the containing package
+ and construct an according module name.
+ The module will be put/looked up in sys.modules.
+ if ensuresyspath is True then the root dir for importing
+ the file (taking __init__.py files into account) will
+ be prepended to sys.path if it isn't there already.
+ If ensuresyspath=="append" the root dir will be appended
+ if it isn't already contained in sys.path.
+ if ensuresyspath is False no modification of syspath happens.
+
+ Special value of ensuresyspath=="importlib" is intended
+ purely for using in pytest, it is capable only of importing
+ separate .py files outside packages, e.g. for test suite
+ without any __init__.py file. It effectively allows having
+ same-named test modules in different places and offers
+ mild opt-in via this option. Note that it works only in
+ recent versions of python.
+ """
+ if not self.check():
+ raise error.ENOENT(self)
+
+ if ensuresyspath == "importlib":
+ if modname is None:
+ modname = self.purebasename
+ spec = importlib.util.spec_from_file_location(modname, str(self))
+ if spec is None or spec.loader is None:
+ raise ImportError(f"Can't find module {modname} at location {self!s}")
+ mod = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(mod)
+ return mod
+
+ pkgpath = None
+ if modname is None:
+ pkgpath = self.pypkgpath()
+ if pkgpath is not None:
+ pkgroot = pkgpath.dirpath()
+ names = self.new(ext="").relto(pkgroot).split(self.sep)
+ if names[-1] == "__init__":
+ names.pop()
+ modname = ".".join(names)
+ else:
+ pkgroot = self.dirpath()
+ modname = self.purebasename
+
+ self._ensuresyspath(ensuresyspath, pkgroot)
+ __import__(modname)
+ mod = sys.modules[modname]
+ if self.basename == "__init__.py":
+ return mod # we don't check anything as we might
+ # be in a namespace package ... too icky to check
+ modfile = mod.__file__
+ assert modfile is not None
+ if modfile[-4:] in (".pyc", ".pyo"):
+ modfile = modfile[:-1]
+ elif modfile.endswith("$py.class"):
+ modfile = modfile[:-9] + ".py"
+ if modfile.endswith(os.sep + "__init__.py"):
+ if self.basename != "__init__.py":
+ modfile = modfile[:-12]
+ try:
+ issame = self.samefile(modfile)
+ except error.ENOENT:
+ issame = False
+ if not issame:
+ ignore = os.getenv("PY_IGNORE_IMPORTMISMATCH")
+ if ignore != "1":
+ raise self.ImportMismatchError(modname, modfile, self)
+ return mod
+ else:
+ try:
+ return sys.modules[modname]
+ except KeyError:
+ # we have a custom modname, do a pseudo-import
+ import types
+
+ mod = types.ModuleType(modname)
+ mod.__file__ = str(self)
+ sys.modules[modname] = mod
+ try:
+ with open(str(self), "rb") as f:
+ exec(f.read(), mod.__dict__)
+ except BaseException:
+ del sys.modules[modname]
+ raise
+ return mod
+
+ def sysexec(self, *argv: os.PathLike[str], **popen_opts: Any) -> str:
+ """Return stdout text from executing a system child process,
+ where the 'self' path points to executable.
+ The process is directly invoked and not through a system shell.
+ """
+ from subprocess import PIPE
+ from subprocess import Popen
+
+ popen_opts.pop("stdout", None)
+ popen_opts.pop("stderr", None)
+ proc = Popen(
+ [str(self)] + [str(arg) for arg in argv],
+ **popen_opts,
+ stdout=PIPE,
+ stderr=PIPE,
+ )
+ stdout: str | bytes
+ stdout, stderr = proc.communicate()
+ ret = proc.wait()
+ if isinstance(stdout, bytes):
+ stdout = stdout.decode(sys.getdefaultencoding())
+ if ret != 0:
+ if isinstance(stderr, bytes):
+ stderr = stderr.decode(sys.getdefaultencoding())
+ raise RuntimeError(
+ ret,
+ ret,
+ str(self),
+ stdout,
+ stderr,
+ )
+ return stdout
+
+ @classmethod
+ def sysfind(cls, name, checker=None, paths=None):
+ """Return a path object found by looking at the systems
+ underlying PATH specification. If the checker is not None
+ it will be invoked to filter matching paths. If a binary
+ cannot be found, None is returned
+ Note: This is probably not working on plain win32 systems
+ but may work on cygwin.
+ """
+ if isabs(name):
+ p = local(name)
+ if p.check(file=1):
+ return p
+ else:
+ if paths is None:
+ if iswin32:
+ paths = os.environ["Path"].split(";")
+ if "" not in paths and "." not in paths:
+ paths.append(".")
+ try:
+ systemroot = os.environ["SYSTEMROOT"]
+ except KeyError:
+ pass
+ else:
+ paths = [
+ path.replace("%SystemRoot%", systemroot) for path in paths
+ ]
+ else:
+ paths = os.environ["PATH"].split(":")
+ tryadd = []
+ if iswin32:
+ tryadd += os.environ["PATHEXT"].split(os.pathsep)
+ tryadd.append("")
+
+ for x in paths:
+ for addext in tryadd:
+ p = local(x).join(name, abs=True) + addext
+ try:
+ if p.check(file=1):
+ if checker:
+ if not checker(p):
+ continue
+ return p
+ except error.EACCES:
+ pass
+ return None
+
+ @classmethod
+ def _gethomedir(cls):
+ try:
+ x = os.environ["HOME"]
+ except KeyError:
+ try:
+ x = os.environ["HOMEDRIVE"] + os.environ["HOMEPATH"]
+ except KeyError:
+ return None
+ return cls(x)
+
+ # """
+ # special class constructors for local filesystem paths
+ # """
+ @classmethod
+ def get_temproot(cls):
+ """Return the system's temporary directory
+ (where tempfiles are usually created in)
+ """
+ import tempfile
+
+ return local(tempfile.gettempdir())
+
+ @classmethod
+ def mkdtemp(cls, rootdir=None):
+ """Return a Path object pointing to a fresh new temporary directory
+ (which we created ourselves).
+ """
+ import tempfile
+
+ if rootdir is None:
+ rootdir = cls.get_temproot()
+ path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
+ return cls(path)
+
+ @classmethod
+ def make_numbered_dir(
+ cls, prefix="session-", rootdir=None, keep=3, lock_timeout=172800
+ ): # two days
+ """Return unique directory with a number greater than the current
+ maximum one. The number is assumed to start directly after prefix.
+ if keep is true directories with a number less than (maxnum-keep)
+ will be removed. If .lock files are used (lock_timeout non-zero),
+ algorithm is multi-process safe.
+ """
+ if rootdir is None:
+ rootdir = cls.get_temproot()
+
+ nprefix = prefix.lower()
+
+ def parse_num(path):
+ """Parse the number out of a path (if it matches the prefix)"""
+ nbasename = path.basename.lower()
+ if nbasename.startswith(nprefix):
+ try:
+ return int(nbasename[len(nprefix) :])
+ except ValueError:
+ pass
+
+ def create_lockfile(path):
+ """Exclusively create lockfile. Throws when failed"""
+ mypid = os.getpid()
+ lockfile = path.join(".lock")
+ if hasattr(lockfile, "mksymlinkto"):
+ lockfile.mksymlinkto(str(mypid))
+ else:
+ fd = error.checked_call(
+ os.open, str(lockfile), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644
+ )
+ with os.fdopen(fd, "w") as f:
+ f.write(str(mypid))
+ return lockfile
+
+ def atexit_remove_lockfile(lockfile):
+ """Ensure lockfile is removed at process exit"""
+ mypid = os.getpid()
+
+ def try_remove_lockfile():
+ # in a fork() situation, only the last process should
+ # remove the .lock, otherwise the other processes run the
+ # risk of seeing their temporary dir disappear. For now
+ # we remove the .lock in the parent only (i.e. we assume
+ # that the children finish before the parent).
+ if os.getpid() != mypid:
+ return
+ try:
+ lockfile.remove()
+ except error.Error:
+ pass
+
+ atexit.register(try_remove_lockfile)
+
+ # compute the maximum number currently in use with the prefix
+ lastmax = None
+ while True:
+ maxnum = -1
+ for path in rootdir.listdir():
+ num = parse_num(path)
+ if num is not None:
+ maxnum = max(maxnum, num)
+
+ # make the new directory
+ try:
+ udir = rootdir.mkdir(prefix + str(maxnum + 1))
+ if lock_timeout:
+ lockfile = create_lockfile(udir)
+ atexit_remove_lockfile(lockfile)
+ except (error.EEXIST, error.ENOENT, error.EBUSY):
+ # race condition (1): another thread/process created the dir
+ # in the meantime - try again
+ # race condition (2): another thread/process spuriously acquired
+ # lock treating empty directory as candidate
+ # for removal - try again
+ # race condition (3): another thread/process tried to create the lock at
+ # the same time (happened in Python 3.3 on Windows)
+ # https://ci.appveyor.com/project/pytestbot/py/build/1.0.21/job/ffi85j4c0lqwsfwa
+ if lastmax == maxnum:
+ raise
+ lastmax = maxnum
+ continue
+ break
+
+ def get_mtime(path):
+ """Read file modification time"""
+ try:
+ return path.lstat().mtime
+ except error.Error:
+ pass
+
+ garbage_prefix = prefix + "garbage-"
+
+ def is_garbage(path):
+ """Check if path denotes directory scheduled for removal"""
+ bn = path.basename
+ return bn.startswith(garbage_prefix)
+
+ # prune old directories
+ udir_time = get_mtime(udir)
+ if keep and udir_time:
+ for path in rootdir.listdir():
+ num = parse_num(path)
+ if num is not None and num <= (maxnum - keep):
+ try:
+ # try acquiring lock to remove directory as exclusive user
+ if lock_timeout:
+ create_lockfile(path)
+ except (error.EEXIST, error.ENOENT, error.EBUSY):
+ path_time = get_mtime(path)
+ if not path_time:
+ # assume directory doesn't exist now
+ continue
+ if abs(udir_time - path_time) < lock_timeout:
+ # assume directory with lockfile exists
+ # and lock timeout hasn't expired yet
+ continue
+
+ # path dir locked for exclusive use
+ # and scheduled for removal to avoid another thread/process
+ # treating it as a new directory or removal candidate
+ garbage_path = rootdir.join(garbage_prefix + str(uuid.uuid4()))
+ try:
+ path.rename(garbage_path)
+ garbage_path.remove(rec=1)
+ except KeyboardInterrupt:
+ raise
+ except Exception: # this might be error.Error, WindowsError ...
+ pass
+ if is_garbage(path):
+ try:
+ path.remove(rec=1)
+ except KeyboardInterrupt:
+ raise
+ except Exception: # this might be error.Error, WindowsError ...
+ pass
+
+ # make link...
+ try:
+ username = os.environ["USER"] # linux, et al
+ except KeyError:
+ try:
+ username = os.environ["USERNAME"] # windows
+ except KeyError:
+ username = "current"
+
+ src = str(udir)
+ dest = src[: src.rfind("-")] + "-" + username
+ try:
+ os.unlink(dest)
+ except OSError:
+ pass
+ try:
+ os.symlink(src, dest)
+ except (OSError, AttributeError, NotImplementedError):
+ pass
+
+ return udir
+
+
+def copymode(src, dest):
+ """Copy permission from src to dst."""
+ import shutil
+
+ shutil.copymode(src, dest)
+
+
+def copystat(src, dest):
+ """Copy permission, last modification time,
+ last access time, and flags from src to dst."""
+ import shutil
+
+ shutil.copystat(str(src), str(dest))
+
+
+def copychunked(src, dest):
+ chunksize = 524288 # half a meg of bytes
+ fsrc = src.open("rb")
+ try:
+ fdest = dest.open("wb")
+ try:
+ while 1:
+ buf = fsrc.read(chunksize)
+ if not buf:
+ break
+ fdest.write(buf)
+ finally:
+ fdest.close()
+ finally:
+ fsrc.close()
+
+
+def isimportable(name):
+ if name and (name[0].isalpha() or name[0] == "_"):
+ name = name.replace("_", "")
+ return not name or name.isalnum()
+
+
+local = LocalPath
diff --git a/venv/lib/python3.11/site-packages/_pytest/_version.py b/venv/lib/python3.11/site-packages/_pytest/_version.py
new file mode 100644
index 000000000..faf5c5516
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/_version.py
@@ -0,0 +1,24 @@
+# file generated by vcs-versioning
+# don't change, don't track in version control
+from __future__ import annotations
+
+__all__ = [
+ "__version__",
+ "__version_tuple__",
+ "version",
+ "version_tuple",
+ "__commit_id__",
+ "commit_id",
+]
+
+version: str
+__version__: str
+__version_tuple__: tuple[int | str, ...]
+version_tuple: tuple[int | str, ...]
+commit_id: str | None
+__commit_id__: str | None
+
+__version__ = version = '9.1.1'
+__version_tuple__ = version_tuple = (9, 1, 1)
+
+__commit_id__ = commit_id = None
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/__init__.py b/venv/lib/python3.11/site-packages/_pytest/assertion/__init__.py
new file mode 100644
index 000000000..e33f8b296
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/__init__.py
@@ -0,0 +1,236 @@
+# mypy: allow-untyped-defs
+"""Support for presenting detailed information in failing assertions."""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+import sys
+from typing import Any
+from typing import Protocol
+from typing import TYPE_CHECKING
+
+from _pytest.assertion import rewrite
+from _pytest.assertion import truncate
+from _pytest.assertion import util
+from _pytest.assertion.rewrite import assertstate_key
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+
+
+if TYPE_CHECKING:
+ from _pytest.main import Session
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--assert",
+ action="store",
+ dest="assertmode",
+ choices=("rewrite", "plain"),
+ default="rewrite",
+ metavar="MODE",
+ help=(
+ "Control assertion debugging tools.\n"
+ "'plain' performs no assertion debugging.\n"
+ "'rewrite' (the default) rewrites assert statements in test modules"
+ " on import to provide assert expression information."
+ ),
+ )
+ parser.addini(
+ "enable_assertion_pass_hook",
+ type="bool",
+ default=False,
+ help="Enables the pytest_assertion_pass hook. "
+ "Make sure to delete any previously generated pyc cache files.",
+ )
+
+ parser.addini(
+ "truncation_limit_lines",
+ default=None,
+ help="Set threshold of LINES after which truncation will take effect",
+ )
+ parser.addini(
+ "truncation_limit_chars",
+ default=None,
+ help=("Set threshold of CHARS after which truncation will take effect"),
+ )
+ parser.addini(
+ "assertion_text_diff_style",
+ default=util.ASSERTION_TEXT_DIFF_STYLE_NDIFF,
+ help=(
+ "Choose how pytest renders diffs for string equality assertions: "
+ f"{util.ASSERTION_TEXT_DIFF_STYLE_NDIFF} or "
+ f"{util.ASSERTION_TEXT_DIFF_STYLE_BLOCK}"
+ ),
+ )
+
+ Config._add_verbosity_ini(
+ parser,
+ Config.VERBOSITY_ASSERTIONS,
+ help=(
+ "Specify a verbosity level for assertions, overriding the main level. "
+ "Higher levels will provide more detailed explanation when an assertion fails."
+ ),
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ util.validate_assertion_text_diff_style(config)
+
+
+def register_assert_rewrite(*names: str) -> None:
+ """Register one or more module names to be rewritten on import.
+
+ This function will make sure that this module or all modules inside
+ the package will get their assert statements rewritten.
+ Thus you should make sure to call this before the module is
+ actually imported, usually in your __init__.py if you are a plugin
+ using a package.
+
+ :param names: The module names to register.
+ """
+ for name in names:
+ if not isinstance(name, str):
+ msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable]
+ raise TypeError(msg.format(repr(names)))
+ rewrite_hook: RewriteHook
+ for hook in sys.meta_path:
+ if isinstance(hook, rewrite.AssertionRewritingHook):
+ rewrite_hook = hook
+ break
+ else:
+ rewrite_hook = DummyRewriteHook()
+ rewrite_hook.mark_rewrite(*names)
+
+
+class RewriteHook(Protocol):
+ def mark_rewrite(self, *names: str) -> None: ...
+
+
+class DummyRewriteHook:
+ """A no-op import hook for when rewriting is disabled."""
+
+ def mark_rewrite(self, *names: str) -> None:
+ pass
+
+
+class AssertionState:
+ """State for the assertion plugin."""
+
+ def __init__(self, config: Config, mode) -> None:
+ self.mode = mode
+ self.trace = config.trace.root.get("assertion")
+ self.hook: rewrite.AssertionRewritingHook | None = None
+
+
+def install_importhook(config: Config) -> rewrite.AssertionRewritingHook:
+ """Try to install the rewrite hook, raise SystemError if it fails."""
+ config.stash[assertstate_key] = AssertionState(config, "rewrite")
+ config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config)
+ sys.meta_path.insert(0, hook)
+ config.stash[assertstate_key].trace("installed rewrite import hook")
+
+ def undo() -> None:
+ hook = config.stash[assertstate_key].hook
+ if hook is not None and hook in sys.meta_path:
+ sys.meta_path.remove(hook)
+
+ config.add_cleanup(undo)
+ return hook
+
+
+def pytest_collection(session: Session) -> None:
+ # This hook is only called when test modules are collected
+ # so for example not in the managing process of pytest-xdist
+ # (which does not collect test modules).
+ assertstate = session.config.stash.get(assertstate_key, None)
+ if assertstate:
+ if assertstate.hook is not None:
+ assertstate.hook.set_session(session)
+
+
+@hookimpl(wrapper=True, tryfirst=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
+ """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
+
+ The rewrite module will use util._reprcompare if it exists to use custom
+ reporting via the pytest_assertrepr_compare hook. This sets up this custom
+ comparison for the test.
+ """
+ ihook = item.ihook
+
+ def callbinrepr(op, left: object, right: object) -> str | None:
+ """Call the pytest_assertrepr_compare hook and prepare the result.
+
+ This uses the first result from the hook and then ensures the
+ following:
+ * Overly verbose explanations are truncated unless configured otherwise
+ (eg. if running in verbose mode).
+ * Embedded newlines are escaped to help util.format_explanation()
+ later.
+ * If the rewrite mode is used embedded %-characters are replaced
+ to protect later % formatting.
+
+ The result can be formatted by util.format_explanation() for
+ pretty printing.
+ """
+ hook_result = ihook.pytest_assertrepr_compare(
+ config=item.config, op=op, left=left, right=right
+ )
+ for new_expl in hook_result:
+ if new_expl:
+ new_expl = truncate.truncate_if_required(new_expl, item)
+ new_expl = [line.replace("\n", "\\n") for line in new_expl]
+ res = "\n~".join(new_expl)
+ if item.config.getvalue("assertmode") == "rewrite":
+ res = res.replace("%", "%%")
+ return res
+ return None
+
+ saved_assert_hooks = util._reprcompare, util._assertion_pass
+ util._reprcompare = callbinrepr
+ util._config = item.config
+
+ if ihook.pytest_assertion_pass.get_hookimpls():
+
+ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None:
+ ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl)
+
+ util._assertion_pass = call_assertion_pass_hook
+
+ try:
+ return (yield)
+ finally:
+ util._reprcompare, util._assertion_pass = saved_assert_hooks
+ util._config = None
+
+
+def pytest_sessionfinish(session: Session) -> None:
+ assertstate = session.config.stash.get(assertstate_key, None)
+ if assertstate:
+ if assertstate.hook is not None:
+ assertstate.hook.set_session(None)
+
+
+def pytest_assertrepr_compare(
+ config: Config, op: str, left: Any, right: Any
+) -> list[str] | None:
+ if config.pluginmanager.has_plugin("terminalreporter"):
+ highlighter = config.get_terminal_writer()._highlight
+ else:
+ # Keep it plaintext when not using terminalrepoterer (#14377).
+ highlighter = util.dummy_highlighter
+ explanation = list(
+ util.assertrepr_compare(
+ op=op,
+ left=left,
+ right=right,
+ verbose=config.get_verbosity(Config.VERBOSITY_ASSERTIONS),
+ highlighter=highlighter,
+ assertion_text_diff_style=util.get_assertion_text_diff_style(config),
+ )
+ )
+ return explanation or None
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_any.py b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_any.py
new file mode 100644
index 000000000..9e5776837
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_any.py
@@ -0,0 +1,131 @@
+from __future__ import annotations
+
+from collections.abc import Iterator
+import dataclasses
+import pprint
+
+from _pytest.assertion._compare_mapping import _compare_eq_mapping
+from _pytest.assertion._compare_sequence import _compare_eq_iterable
+from _pytest.assertion._compare_sequence import _compare_eq_sequence
+from _pytest.assertion._compare_set import _compare_eq_set
+from _pytest.assertion._guards import has_default_eq
+from _pytest.assertion._guards import isattrs
+from _pytest.assertion._guards import isdatacls
+from _pytest.assertion._guards import isiterable
+from _pytest.assertion._guards import ismapping
+from _pytest.assertion._guards import isnamedtuple
+from _pytest.assertion._guards import issequence
+from _pytest.assertion._guards import isset
+from _pytest.assertion._guards import istext
+from _pytest.assertion._typing import _AssertionTextDiffStyle
+from _pytest.assertion._typing import _HighlightFunc
+from _pytest.assertion.compare_text import _compare_eq_text
+
+
+def _compare_eq_any(
+ left: object,
+ right: object,
+ highlighter: _HighlightFunc,
+ verbose: int,
+ assertion_text_diff_style: _AssertionTextDiffStyle,
+) -> Iterator[str]:
+ """Yield the per-line explanation for ``left == right`` (without summary).
+
+ Yields nothing when no specialised explanation applies, so consumers
+ can stream the output and bail out early (e.g. for truncation) without
+ materialising the entire diff first.
+ """
+ if istext(left) and istext(right):
+ yield from _compare_eq_text(
+ left,
+ right,
+ highlighter,
+ verbose,
+ assertion_text_diff_style,
+ )
+ else:
+ from _pytest.python_api import ApproxBase
+
+ # Although the common order should be obtained == approx(...), allow both ways.
+ if isinstance(right, ApproxBase):
+ yield from right._repr_compare(left)
+ elif isinstance(left, ApproxBase):
+ yield from left._repr_compare(right)
+ elif type(left) is type(right) and (
+ isdatacls(left) or isattrs(left) or isnamedtuple(left)
+ ):
+ # Note: unlike dataclasses/attrs, namedtuples compare only the
+ # field values, not the type or field names. But this branch
+ # intentionally only handles the same-type case, which was often
+ # used in older code bases before dataclasses/attrs were available.
+ yield from _compare_eq_cls(
+ left,
+ right,
+ highlighter,
+ verbose,
+ assertion_text_diff_style,
+ )
+ elif issequence(left) and issequence(right):
+ yield from _compare_eq_sequence(left, right, highlighter, verbose)
+ elif isset(left) and isset(right):
+ yield from _compare_eq_set(left, right, highlighter, verbose)
+ elif ismapping(left) and ismapping(right):
+ yield from _compare_eq_mapping(left, right, highlighter, verbose)
+
+ if isiterable(left) and isiterable(right):
+ yield from _compare_eq_iterable(left, right, highlighter, verbose)
+
+
+def _compare_eq_cls(
+ left: object,
+ right: object,
+ highlighter: _HighlightFunc,
+ verbose: int,
+ assertion_text_diff_style: _AssertionTextDiffStyle,
+) -> Iterator[str]:
+ if not has_default_eq(left):
+ return
+ if isdatacls(left):
+ all_fields = dataclasses.fields(left)
+ fields_to_check = [info.name for info in all_fields if info.compare]
+ elif isattrs(left):
+ all_fields = left.__attrs_attrs__ # type: ignore[attr-defined]
+ fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
+ elif isnamedtuple(left):
+ fields_to_check = left._fields # type: ignore[attr-defined]
+ else:
+ assert False
+
+ indent = " "
+ same = []
+ diff = []
+ for field in fields_to_check:
+ if getattr(left, field) == getattr(right, field):
+ same.append(field)
+ else:
+ diff.append(field)
+
+ if same or diff:
+ yield ""
+ if same and verbose < 2:
+ yield f"Omitting {len(same)} identical items, use -vv to show"
+ elif same:
+ yield "Matching attributes:"
+ yield from highlighter(pprint.pformat(same)).splitlines()
+ if diff:
+ yield "Differing attributes:"
+ yield from highlighter(pprint.pformat(diff)).splitlines()
+ for field in diff:
+ field_left = getattr(left, field)
+ field_right = getattr(right, field)
+ yield ""
+ yield f"Drill down into differing attribute {field}:"
+ yield f"{indent}{field}: {highlighter(repr(field_left))} != {highlighter(repr(field_right))}"
+ for line in _compare_eq_any(
+ field_left,
+ field_right,
+ highlighter,
+ verbose,
+ assertion_text_diff_style,
+ ):
+ yield indent + line
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_mapping.py b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_mapping.py
new file mode 100644
index 000000000..4edb47026
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_mapping.py
@@ -0,0 +1,48 @@
+from __future__ import annotations
+
+from collections.abc import Iterator
+from collections.abc import Mapping
+import pprint
+
+from _pytest._io.saferepr import saferepr
+from _pytest.assertion._typing import _HighlightFunc
+
+
+def _compare_eq_mapping(
+ left: Mapping[object, object],
+ right: Mapping[object, object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ set_left = set(left)
+ set_right = set(right)
+ common = set_left.intersection(set_right)
+ same = {k: left[k] for k in common if left[k] == right[k]}
+ if same and verbose < 2:
+ yield f"Omitting {len(same)} identical items, use -vv to show"
+ elif same:
+ yield "Common items:"
+ yield from highlighter(pprint.pformat(same)).splitlines()
+ diff = {k for k in common if left[k] != right[k]}
+ if diff:
+ yield "Differing items:"
+ for k in diff:
+ yield (
+ highlighter(saferepr({k: left[k]}))
+ + " != "
+ + highlighter(saferepr({k: right[k]}))
+ )
+ extra_left = set_left - set_right
+ len_extra_left = len(extra_left)
+ if len_extra_left:
+ yield f"Left contains {len_extra_left} more item{'' if len_extra_left == 1 else 's'}:"
+ yield from highlighter(
+ pprint.pformat({k: left[k] for k in extra_left})
+ ).splitlines()
+ extra_right = set_right - set_left
+ len_extra_right = len(extra_right)
+ if len_extra_right:
+ yield f"Right contains {len_extra_right} more item{'' if len_extra_right == 1 else 's'}:"
+ yield from highlighter(
+ pprint.pformat({k: right[k] for k in extra_right})
+ ).splitlines()
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_sequence.py b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_sequence.py
new file mode 100644
index 000000000..cd0043bf7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_sequence.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Sequence
+
+from _pytest._io.pprint import PrettyPrinter
+from _pytest._io.saferepr import saferepr
+from _pytest.assertion._typing import _HighlightFunc
+from _pytest.compat import running_on_ci
+
+
+def _compare_eq_iterable(
+ left: Iterable[object],
+ right: Iterable[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ if verbose <= 0 and not running_on_ci():
+ yield "Use -v to get more diff"
+ return
+ # dynamic import to speedup pytest
+ import difflib
+
+ left_formatting = PrettyPrinter().pformat(left).splitlines()
+ right_formatting = PrettyPrinter().pformat(right).splitlines()
+
+ yield ""
+ yield "Full diff:"
+ # "right" is the expected base against which we compare "left",
+ # see https://github.com/pytest-dev/pytest/issues/3333
+ yield from highlighter(
+ "\n".join(
+ line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting)
+ ),
+ lexer="diff",
+ ).splitlines()
+
+
+def _compare_eq_sequence(
+ left: Sequence[object],
+ right: Sequence[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes)
+ len_left = len(left)
+ len_right = len(right)
+ for i in range(min(len_left, len_right)):
+ if left[i] != right[i]:
+ if comparing_bytes:
+ # when comparing bytes, we want to see their ascii representation
+ # instead of their numeric values (#5260)
+ # using a slice gives us the ascii representation:
+ # >>> s = b'foo'
+ # >>> s[0]
+ # 102
+ # >>> s[0:1]
+ # b'f'
+ left_value: object = left[i : i + 1]
+ right_value: object = right[i : i + 1]
+ else:
+ left_value = left[i]
+ right_value = right[i]
+
+ yield (
+ f"At index {i} diff:"
+ f" {highlighter(repr(left_value))} != {highlighter(repr(right_value))}"
+ )
+ break
+
+ if comparing_bytes:
+ # when comparing bytes, it doesn't help to show the "sides contain one or more
+ # items" longer explanation, so skip it
+ return
+
+ len_diff = len_left - len_right
+ if len_diff:
+ if len_diff > 0:
+ dir_with_more = "Left"
+ extra = saferepr(left[len_right])
+ else:
+ len_diff = 0 - len_diff
+ dir_with_more = "Right"
+ extra = saferepr(right[len_left])
+
+ if len_diff == 1:
+ yield f"{dir_with_more} contains one more item: {highlighter(extra)}"
+ else:
+ yield f"{dir_with_more} contains {len_diff} more items, first extra item: {highlighter(extra)}"
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_set.py b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_set.py
new file mode 100644
index 000000000..281713379
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/_compare_set.py
@@ -0,0 +1,101 @@
+from __future__ import annotations
+
+from collections.abc import Callable
+from collections.abc import Iterator
+from collections.abc import Set as AbstractSet
+from typing import TypeAlias
+
+from _pytest._io.saferepr import saferepr
+from _pytest.assertion._typing import _HighlightFunc
+
+
+def _set_one_sided_diff(
+ posn: str,
+ set1: AbstractSet[object],
+ set2: AbstractSet[object],
+ highlighter: _HighlightFunc,
+) -> Iterator[str]:
+ diff = set1 - set2
+ if diff:
+ yield f"Extra items in the {posn} set:"
+ for item in diff:
+ yield highlighter(saferepr(item))
+
+
+def _compare_eq_set(
+ left: AbstractSet[object],
+ right: AbstractSet[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ yield from _set_one_sided_diff("left", left, right, highlighter)
+ yield from _set_one_sided_diff("right", right, left, highlighter)
+
+
+def _compare_gte_set(
+ left: AbstractSet[object],
+ right: AbstractSet[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ yield from _set_one_sided_diff("right", right, left, highlighter)
+
+
+def _compare_lte_set(
+ left: AbstractSet[object],
+ right: AbstractSet[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ yield from _set_one_sided_diff("left", left, right, highlighter)
+
+
+def _compare_gt_set(
+ left: AbstractSet[object],
+ right: AbstractSet[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ if left == right:
+ yield "Both sets are equal"
+ else:
+ yield from _set_one_sided_diff("right", right, left, highlighter)
+
+
+def _compare_lt_set(
+ left: AbstractSet[object],
+ right: AbstractSet[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ if left == right:
+ yield "Both sets are equal"
+ else:
+ yield from _set_one_sided_diff("left", left, right, highlighter)
+
+
+SetComparisonFunction: TypeAlias = Callable[
+ [AbstractSet[object], AbstractSet[object], _HighlightFunc, int],
+ Iterator[str],
+]
+
+
+def _both_sets_are_equal(
+ left: AbstractSet[object],
+ right: AbstractSet[object],
+ highlighter: _HighlightFunc,
+ verbose: int = 0,
+) -> Iterator[str]:
+ yield "Both sets are equal"
+
+
+SET_COMPARISON_FUNCTIONS: dict[str, SetComparisonFunction] = {
+ # == can't be done here without a prior refactor because there's an additional
+ # explanation for iterable in _compare_eq_any
+ # "==": _compare_eq_set,
+ "!=": _both_sets_are_equal,
+ ">=": _compare_gte_set,
+ "<=": _compare_lte_set,
+ ">": _compare_gt_set,
+ "<": _compare_lt_set,
+}
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/_guards.py b/venv/lib/python3.11/site-packages/_pytest/assertion/_guards.py
new file mode 100644
index 000000000..bb9fedd6e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/_guards.py
@@ -0,0 +1,60 @@
+from __future__ import annotations
+
+import collections.abc
+from collections.abc import Mapping
+import dataclasses
+from typing import TypeGuard
+
+
+def issequence(x: object) -> TypeGuard[collections.abc.Sequence[object]]:
+ return isinstance(x, collections.abc.Sequence) and not isinstance(x, str)
+
+
+def istext(x: object) -> TypeGuard[str]:
+ return isinstance(x, str)
+
+
+def ismapping(x: object) -> TypeGuard[Mapping[object, object]]:
+ return isinstance(x, Mapping)
+
+
+def isset(x: object) -> TypeGuard[set[object] | frozenset[object]]:
+ return isinstance(x, set | frozenset)
+
+
+def isnamedtuple(obj: object) -> bool:
+ return isinstance(obj, tuple) and getattr(obj, "_fields", None) is not None
+
+
+isdatacls = dataclasses.is_dataclass
+
+
+def isattrs(obj: object) -> bool:
+ return getattr(obj, "__attrs_attrs__", None) is not None
+
+
+def isiterable(obj: object) -> TypeGuard[collections.abc.Iterable[object]]:
+ try:
+ iter(obj) # type: ignore[call-overload]
+ return not istext(obj)
+ except Exception:
+ return False
+
+
+def has_default_eq(obj: object) -> bool:
+ """Check if an instance of an object contains the default eq
+
+ First, we check if the object's __eq__ attribute has __code__,
+ if so, we check the equally of the method code filename (__code__.co_filename)
+ to the default one generated by the dataclass and attr module
+ for dataclasses the default co_filename is , for attrs class, the __eq__ should contain "attrs eq generated"
+ """
+ # inspired from https://github.com/willmcgugan/rich/blob/07d51ffc1aee6f16bd2e5a25b4e82850fb9ed778/rich/pretty.py#L68
+ if hasattr(obj.__eq__, "__code__") and hasattr(obj.__eq__.__code__, "co_filename"):
+ code_filename = obj.__eq__.__code__.co_filename
+
+ if isattrs(obj):
+ return "attrs generated " in code_filename
+
+ return code_filename == "" # data class
+ return True
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/_typing.py b/venv/lib/python3.11/site-packages/_pytest/assertion/_typing.py
new file mode 100644
index 000000000..f032f34e0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/_typing.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from typing import Literal
+from typing import Protocol
+
+
+_AssertionTextDiffStyle = Literal["ndiff", "block"]
+
+
+class _HighlightFunc(Protocol): # noqa: PYI046
+ def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
+ """Apply highlighting to the given source."""
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/compare_text.py b/venv/lib/python3.11/site-packages/_pytest/assertion/compare_text.py
new file mode 100644
index 000000000..31096444b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/compare_text.py
@@ -0,0 +1,104 @@
+from __future__ import annotations
+
+from collections.abc import Iterator
+
+from _pytest._io.saferepr import saferepr
+from _pytest.assertion._typing import _AssertionTextDiffStyle
+from _pytest.assertion._typing import _HighlightFunc
+from _pytest.assertion.highlight import dummy_highlighter
+from _pytest.compat import assert_never
+
+
+def _compare_eq_text(
+ left: str,
+ right: str,
+ highlighter: _HighlightFunc,
+ verbose: int,
+ assertion_text_diff_style: _AssertionTextDiffStyle,
+) -> Iterator[str]:
+ match assertion_text_diff_style:
+ case "block":
+ yield from _diff_text_block(left, right)
+ case "ndiff":
+ yield from _diff_text(left, right, highlighter, verbose)
+ case unreachable:
+ assert_never(unreachable)
+
+
+def _diff_text_block(left: str, right: str) -> Iterator[str]:
+ yield "Left:"
+ yield from _format_text_block_lines(left)
+ yield ""
+ yield "Right:"
+ yield from _format_text_block_lines(right)
+
+
+def _format_text_block_lines(text: str) -> Iterator[str]:
+ for line in text.split("\n"):
+ yield f" {line}"
+
+
+def _diff_text(
+ left: str, right: str, highlighter: _HighlightFunc, verbose: int = 0
+) -> Iterator[str]:
+ """Yield the explanation for the diff between text.
+
+ Unless --verbose is used this will skip leading and trailing
+ characters which are identical to keep the diff minimal.
+ """
+ from difflib import ndiff
+
+ if verbose < 1:
+ i = 0 # just in case left or right has zero length
+ for i in range(min(len(left), len(right))):
+ if left[i] != right[i]:
+ break
+ if i > 42:
+ i -= 10 # Provide some context
+ yield f"Skipping {i} identical leading characters in diff, use -v to show"
+ left = left[i:]
+ right = right[i:]
+ if len(left) == len(right):
+ for i in range(len(left)):
+ if left[-i] != right[-i]:
+ break
+ if i > 42:
+ i -= 10 # Provide some context
+ yield (
+ f"Skipping {i} identical trailing "
+ "characters in diff, use -v to show"
+ )
+ left = left[:-i]
+ right = right[:-i]
+ keepends = True
+ if left.isspace() or right.isspace():
+ left = repr(str(left))
+ right = repr(str(right))
+ yield "Strings contain only whitespace, escaping them using repr()"
+ # "right" is the expected base against which we compare "left",
+ # see https://github.com/pytest-dev/pytest/issues/3333
+ yield from highlighter(
+ "\n".join(
+ line.strip("\n")
+ for line in ndiff(right.splitlines(keepends), left.splitlines(keepends))
+ ),
+ lexer="diff",
+ ).splitlines()
+
+
+def _notin_text(term: str, text: str, verbose: int = 0) -> Iterator[str]:
+ index = text.find(term)
+ head = text[:index]
+ tail = text[index + len(term) :]
+ correct_text = head + tail
+ diff = _diff_text(text, correct_text, dummy_highlighter, verbose)
+ yield f"{saferepr(term, maxsize=42)} is contained here:"
+ for line in diff:
+ if line.startswith("Skipping"):
+ continue
+ if line.startswith("- "):
+ continue
+ if line.startswith("+ "):
+ yield " " + line[2:]
+ else:
+ yield line
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/highlight.py b/venv/lib/python3.11/site-packages/_pytest/assertion/highlight.py
new file mode 100644
index 000000000..9ae833f19
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/highlight.py
@@ -0,0 +1,11 @@
+from __future__ import annotations
+
+from typing import Literal
+
+
+def dummy_highlighter(source: str, lexer: Literal["diff", "python"] = "python") -> str:
+ """Dummy highlighter that returns the text unprocessed.
+
+ Needed for _notin_text, as the diff gets post-processed to only show the "+" part.
+ """
+ return source
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/rewrite.py b/venv/lib/python3.11/site-packages/_pytest/assertion/rewrite.py
new file mode 100644
index 000000000..99815b70c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/rewrite.py
@@ -0,0 +1,1193 @@
+"""Rewrite assertion AST to produce nice error messages."""
+
+from __future__ import annotations
+
+import ast
+from collections import defaultdict
+from collections.abc import Callable
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Sequence
+import errno
+import functools
+import importlib.abc
+import importlib.machinery
+import importlib.util
+import io
+import itertools
+import marshal
+import os
+from pathlib import Path
+from pathlib import PurePath
+import struct
+import sys
+import tokenize
+import types
+from typing import IO
+from typing import TYPE_CHECKING
+
+
+if sys.version_info >= (3, 12):
+ from importlib.resources.abc import TraversableResources
+else:
+ from importlib.abc import TraversableResources
+if sys.version_info < (3, 11):
+ from importlib.readers import FileReader
+else:
+ from importlib.resources.readers import FileReader
+
+
+from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
+from _pytest._io.saferepr import saferepr
+from _pytest._io.saferepr import saferepr_unlimited
+from _pytest._version import version
+from _pytest.assertion import util
+from _pytest.config import Config
+from _pytest.fixtures import FixtureFunctionDefinition
+from _pytest.main import Session
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import fnmatch_ex
+from _pytest.stash import StashKey
+
+
+# fmt: off
+from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
+# fmt:on
+
+if TYPE_CHECKING:
+ from _pytest.assertion import AssertionState
+
+
+class Sentinel:
+ pass
+
+
+assertstate_key = StashKey["AssertionState"]()
+
+# pytest caches rewritten pycs in pycache dirs
+PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
+PYC_EXT = ".py" + ((__debug__ and "c") or "o")
+PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
+
+# Special marker that denotes we have just left a scope definition
+_SCOPE_END_MARKER = Sentinel()
+
+
+class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
+ """PEP302/PEP451 import hook which rewrites asserts."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ try:
+ self.fnpats = config.getini("python_files")
+ except ValueError:
+ self.fnpats = ["test_*.py", "*_test.py"]
+ self.session: Session | None = None
+ self._rewritten_names: dict[str, Path] = {}
+ self._must_rewrite: set[str] = set()
+ # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
+ # which might result in infinite recursion (#3506)
+ self._writing_pyc = False
+ self._basenames_to_check_rewrite = {"conftest"}
+ self._marked_for_rewrite_cache: dict[str, bool] = {}
+ self._session_paths_checked = False
+
+ def set_session(self, session: Session | None) -> None:
+ self.session = session
+ self._session_paths_checked = False
+
+ # Indirection so we can mock calls to find_spec originated from the hook during testing
+ _find_spec = importlib.machinery.PathFinder.find_spec
+
+ def find_spec(
+ self,
+ name: str,
+ path: Sequence[str | bytes] | None = None,
+ target: types.ModuleType | None = None,
+ ) -> importlib.machinery.ModuleSpec | None:
+ if self._writing_pyc:
+ return None
+ state = self.config.stash[assertstate_key]
+ if self._early_rewrite_bailout(name, state):
+ return None
+ state.trace(f"find_module called for: {name}")
+
+ # Type ignored because mypy is confused about the `self` binding here.
+ spec = self._find_spec(name, path) # type: ignore
+
+ if (
+ # the import machinery could not find a file to import
+ spec is None
+ # this is a namespace package (without `__init__.py`)
+ # there's nothing to rewrite there
+ or spec.origin is None
+ # we can only rewrite source files
+ or not isinstance(spec.loader, importlib.machinery.SourceFileLoader)
+ # if the file doesn't exist, we can't rewrite it
+ or not os.path.exists(spec.origin)
+ ):
+ return None
+ else:
+ fn = spec.origin
+
+ if not self._should_rewrite(name, fn, state):
+ return None
+
+ return importlib.util.spec_from_file_location(
+ name,
+ fn,
+ loader=self,
+ submodule_search_locations=spec.submodule_search_locations,
+ )
+
+ def create_module(
+ self, spec: importlib.machinery.ModuleSpec
+ ) -> types.ModuleType | None:
+ return None # default behaviour is fine
+
+ def exec_module(self, module: types.ModuleType) -> None:
+ assert module.__spec__ is not None
+ assert module.__spec__.origin is not None
+ fn = Path(module.__spec__.origin)
+ state = self.config.stash[assertstate_key]
+
+ self._rewritten_names[module.__name__] = fn
+
+ # The requested module looks like a test file, so rewrite it. This is
+ # the most magical part of the process: load the source, rewrite the
+ # asserts, and load the rewritten source. We also cache the rewritten
+ # module code in a special pyc. We must be aware of the possibility of
+ # concurrent pytest processes rewriting and loading pycs. To avoid
+ # tricky race conditions, we maintain the following invariant: The
+ # cached pyc is always a complete, valid pyc. Operations on it must be
+ # atomic. POSIX's atomic rename comes in handy.
+ write = not sys.dont_write_bytecode
+ cache_dir = get_cache_dir(fn)
+ if write:
+ ok = try_makedirs(cache_dir)
+ if not ok:
+ write = False
+ state.trace(f"read only directory: {cache_dir}")
+
+ cache_name = fn.name[:-3] + PYC_TAIL
+ pyc = cache_dir / cache_name
+ # Notice that even if we're in a read-only directory, I'm going
+ # to check for a cached pyc. This may not be optimal...
+ co = _read_pyc(fn, pyc, state.trace)
+ if co is None:
+ state.trace(f"rewriting {fn!r}")
+ source_stat, co = _rewrite_test(fn, self.config)
+ if write:
+ self._writing_pyc = True
+ try:
+ _write_pyc(state, co, source_stat, pyc)
+ finally:
+ self._writing_pyc = False
+ else:
+ state.trace(f"found cached rewritten pyc for {fn}")
+ exec(co, module.__dict__)
+
+ def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool:
+ """A fast way to get out of rewriting modules.
+
+ Profiling has shown that the call to PathFinder.find_spec (inside of
+ the find_spec from this class) is a major slowdown, so, this method
+ tries to filter what we're sure won't be rewritten before getting to
+ it.
+ """
+ if self.session is not None and not self._session_paths_checked:
+ self._session_paths_checked = True
+ for initial_path in self.session._initialpaths:
+ # Make something as c:/projects/my_project/path.py ->
+ # ['c:', 'projects', 'my_project', 'path.py']
+ parts = str(initial_path).split(os.sep)
+ # add 'path' to basenames to be checked.
+ self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
+
+ # Note: conftest already by default in _basenames_to_check_rewrite.
+ parts = name.split(".")
+ if parts[-1] in self._basenames_to_check_rewrite:
+ return False
+
+ # For matching the name it must be as if it was a filename.
+ path = PurePath(*parts).with_suffix(".py")
+
+ for pat in self.fnpats:
+ # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
+ # on the name alone because we need to match against the full path
+ if os.path.dirname(pat):
+ return False
+ if fnmatch_ex(pat, path):
+ return False
+
+ if self._is_marked_for_rewrite(name, state):
+ return False
+
+ state.trace(f"early skip of rewriting module: {name}")
+ return True
+
+ def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool:
+ # always rewrite conftest files
+ if os.path.basename(fn) == "conftest.py":
+ state.trace(f"rewriting conftest file: {fn!r}")
+ return True
+
+ if self.session is not None:
+ if self.session.isinitpath(absolutepath(fn)):
+ state.trace(f"matched test file (was specified on cmdline): {fn!r}")
+ return True
+
+ # modules not passed explicitly on the command line are only
+ # rewritten if they match the naming convention for test files
+ fn_path = PurePath(fn)
+ for pat in self.fnpats:
+ if fnmatch_ex(pat, fn_path):
+ state.trace(f"matched test file {fn!r}")
+ return True
+
+ return self._is_marked_for_rewrite(name, state)
+
+ def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool:
+ try:
+ return self._marked_for_rewrite_cache[name]
+ except KeyError:
+ for marked in self._must_rewrite:
+ if name == marked or name.startswith(marked + "."):
+ state.trace(f"matched marked file {name!r} (from {marked!r})")
+ self._marked_for_rewrite_cache[name] = True
+ return True
+
+ self._marked_for_rewrite_cache[name] = False
+ return False
+
+ def mark_rewrite(self, *names: str) -> None:
+ """Mark import names as needing to be rewritten.
+
+ The named module or package as well as any nested modules will
+ be rewritten on import.
+ """
+ already_imported = (
+ set(names).intersection(sys.modules).difference(self._rewritten_names)
+ )
+ for name in already_imported:
+ mod = sys.modules[name]
+ if not AssertionRewriter.is_rewrite_disabled(
+ mod.__doc__ or ""
+ ) and not isinstance(mod.__loader__, type(self)):
+ self._warn_already_imported(name)
+ self._must_rewrite.update(names)
+ self._marked_for_rewrite_cache.clear()
+
+ def _warn_already_imported(self, name: str) -> None:
+ from _pytest.warning_types import PytestAssertRewriteWarning
+
+ self.config.issue_config_time_warning(
+ PytestAssertRewriteWarning(
+ f"Module already imported so cannot be rewritten; {name}"
+ ),
+ stacklevel=5,
+ )
+
+ def get_data(self, pathname: str | bytes) -> bytes:
+ """Optional PEP302 get_data API."""
+ with open(pathname, "rb") as f:
+ return f.read()
+
+ def get_resource_reader(self, name: str) -> TraversableResources:
+ return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) # type: ignore[arg-type]
+
+
+def _write_pyc_fp(
+ fp: IO[bytes], source_stat: os.stat_result, co: types.CodeType
+) -> None:
+ # Technically, we don't have to have the same pyc format as
+ # (C)Python, since these "pycs" should never be seen by builtin
+ # import. However, there's little reason to deviate.
+ fp.write(importlib.util.MAGIC_NUMBER)
+ # https://www.python.org/dev/peps/pep-0552/
+ flags = b"\x00\x00\x00\x00"
+ fp.write(flags)
+ # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
+ mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
+ size = source_stat.st_size & 0xFFFFFFFF
+ # " bool:
+ proc_pyc = f"{pyc}.{os.getpid()}"
+ try:
+ with open(proc_pyc, "wb") as fp:
+ _write_pyc_fp(fp, source_stat, co)
+ except OSError as e:
+ state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
+ return False
+
+ try:
+ os.replace(proc_pyc, pyc)
+ except OSError as e:
+ state.trace(f"error writing pyc file at {pyc}: {e}")
+ # we ignore any failure to write the cache file
+ # there are many reasons, permission-denied, pycache dir being a
+ # file etc.
+ return False
+ return True
+
+
+def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]:
+ """Read and rewrite *fn* and return the code object."""
+ stat = os.stat(fn)
+ source = fn.read_bytes()
+ strfn = str(fn)
+ tree = ast.parse(source, filename=strfn)
+ rewrite_asserts(tree, source, strfn, config)
+ co = compile(tree, strfn, "exec", dont_inherit=True)
+ return stat, co
+
+
+def _read_pyc(
+ source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None
+) -> types.CodeType | None:
+ """Possibly read a pytest pyc containing rewritten code.
+
+ Return rewritten code if successful or None if not.
+ """
+ try:
+ fp = open(pyc, "rb")
+ except OSError:
+ return None
+ with fp:
+ try:
+ stat_result = os.stat(source)
+ mtime = int(stat_result.st_mtime)
+ size = stat_result.st_size
+ data = fp.read(16)
+ except OSError as e:
+ trace(f"_read_pyc({source}): OSError {e}")
+ return None
+ # Check for invalid or out of date pyc file.
+ if len(data) != (16):
+ trace(f"_read_pyc({source}): invalid pyc (too short)")
+ return None
+ if data[:4] != importlib.util.MAGIC_NUMBER:
+ trace(f"_read_pyc({source}): invalid pyc (bad magic number)")
+ return None
+ if data[4:8] != b"\x00\x00\x00\x00":
+ trace(f"_read_pyc({source}): invalid pyc (unsupported flags)")
+ return None
+ mtime_data = data[8:12]
+ if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
+ trace(f"_read_pyc({source}): out of date")
+ return None
+ size_data = data[12:16]
+ if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
+ trace(f"_read_pyc({source}): invalid pyc (incorrect size)")
+ return None
+ try:
+ co = marshal.load(fp)
+ except Exception as e:
+ trace(f"_read_pyc({source}): marshal.load error {e}")
+ return None
+ if not isinstance(co, types.CodeType):
+ trace(f"_read_pyc({source}): not a code object")
+ return None
+ return co
+
+
+def rewrite_asserts(
+ mod: ast.Module,
+ source: bytes,
+ module_path: str | None = None,
+ config: Config | None = None,
+) -> None:
+ """Rewrite the assert statements in mod."""
+ AssertionRewriter(module_path, config, source).run(mod)
+
+
+def _saferepr(obj: object) -> str:
+ r"""Get a safe repr of an object for assertion error messages.
+
+ The assertion formatting (util.format_explanation()) requires
+ newlines to be escaped since they are a special character for it.
+ Normally assertion.util.format_explanation() does this but for a
+ custom repr it is possible to contain one of the special escape
+ sequences, especially '\n{' and '\n}' are likely to be present in
+ JSON reprs.
+ """
+ if isinstance(obj, types.MethodType):
+ # for bound methods, skip redundant information
+ return obj.__name__
+
+ maxsize = _get_maxsize_for_saferepr(util._config)
+ if not maxsize:
+ return saferepr_unlimited(obj).replace("\n", "\\n")
+ return saferepr(obj, maxsize=maxsize).replace("\n", "\\n")
+
+
+def _get_maxsize_for_saferepr(config: Config | None) -> int | None:
+ """Get `maxsize` configuration for saferepr based on the given config object."""
+ if config is None:
+ verbosity = 0
+ else:
+ verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
+ if verbosity >= 2:
+ return None
+ if verbosity >= 1:
+ return DEFAULT_REPR_MAX_SIZE * 10
+ return DEFAULT_REPR_MAX_SIZE
+
+
+def _format_assertmsg(obj: object) -> str:
+ r"""Format the custom assertion message given.
+
+ For strings this simply replaces newlines with '\n~' so that
+ util.format_explanation() will preserve them instead of escaping
+ newlines. For other objects saferepr() is used first.
+ """
+ # reprlib appears to have a bug which means that if a string
+ # contains a newline it gets escaped, however if an object has a
+ # .__repr__() which contains newlines it does not get escaped.
+ # However in either case we want to preserve the newline.
+ replaces = [("\n", "\n~"), ("%", "%%")]
+ if not isinstance(obj, str):
+ obj = saferepr(obj, _get_maxsize_for_saferepr(util._config))
+ replaces.append(("\\n", "\n~"))
+
+ for r1, r2 in replaces:
+ obj = obj.replace(r1, r2)
+
+ return obj
+
+
+def _should_repr_global_name(obj: object) -> bool:
+ if callable(obj):
+ # For pytest fixtures the __repr__ method provides more information than the function name.
+ return isinstance(obj, FixtureFunctionDefinition)
+
+ try:
+ return not hasattr(obj, "__name__")
+ except Exception:
+ return True
+
+
+def _format_boolop(explanations: Iterable[str], is_or: bool) -> str:
+ explanation = "(" + ((is_or and " or ") or " and ").join(explanations) + ")"
+ return explanation.replace("%", "%%")
+
+
+def _call_reprcompare(
+ ops: Sequence[str],
+ results: Sequence[bool],
+ expls: Sequence[str],
+ each_obj: Sequence[object],
+) -> str:
+ for i, res, expl in zip(range(len(ops)), results, expls, strict=True):
+ try:
+ done = not res
+ except Exception:
+ done = True
+ if done:
+ break
+ if util._reprcompare is not None:
+ custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
+ if custom is not None:
+ return custom
+ return expl
+
+
+def _call_assertion_pass(lineno: int, orig: str, expl: str) -> None:
+ if util._assertion_pass is not None:
+ util._assertion_pass(lineno, orig, expl)
+
+
+def _check_if_assertion_pass_impl() -> bool:
+ """Check if any plugins implement the pytest_assertion_pass hook
+ in order not to generate explanation unnecessarily (might be expensive)."""
+ return True if util._assertion_pass else False
+
+
+UNARY_MAP = {ast.Not: "not %s", ast.Invert: "~%s", ast.USub: "-%s", ast.UAdd: "+%s"}
+
+BINOP_MAP = {
+ ast.BitOr: "|",
+ ast.BitXor: "^",
+ ast.BitAnd: "&",
+ ast.LShift: "<<",
+ ast.RShift: ">>",
+ ast.Add: "+",
+ ast.Sub: "-",
+ ast.Mult: "*",
+ ast.Div: "/",
+ ast.FloorDiv: "//",
+ ast.Mod: "%%", # escaped for string formatting
+ ast.Eq: "==",
+ ast.NotEq: "!=",
+ ast.Lt: "<",
+ ast.LtE: "<=",
+ ast.Gt: ">",
+ ast.GtE: ">=",
+ ast.Pow: "**",
+ ast.Is: "is",
+ ast.IsNot: "is not",
+ ast.In: "in",
+ ast.NotIn: "not in",
+ ast.MatMult: "@",
+}
+
+
+def traverse_node(node: ast.AST) -> Iterator[ast.AST]:
+ """Recursively yield node and all its children in depth-first order."""
+ yield node
+ for child in ast.iter_child_nodes(node):
+ yield from traverse_node(child)
+
+
+@functools.lru_cache(maxsize=1)
+def _get_assertion_exprs(src: bytes) -> dict[int, str]:
+ """Return a mapping from {lineno: "assertion test expression"}."""
+ ret: dict[int, str] = {}
+
+ depth = 0
+ lines: list[str] = []
+ assert_lineno: int | None = None
+ seen_lines: set[int] = set()
+
+ def _write_and_reset() -> None:
+ nonlocal depth, lines, assert_lineno, seen_lines
+ assert assert_lineno is not None
+ ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\")
+ depth = 0
+ lines = []
+ assert_lineno = None
+ seen_lines = set()
+
+ tokens = tokenize.tokenize(io.BytesIO(src).readline)
+ for tp, source, (lineno, offset), _, line in tokens:
+ if tp == tokenize.NAME and source == "assert":
+ assert_lineno = lineno
+ elif assert_lineno is not None:
+ # keep track of depth for the assert-message `,` lookup
+ if tp == tokenize.OP and source in "([{":
+ depth += 1
+ elif tp == tokenize.OP and source in ")]}":
+ depth -= 1
+
+ if not lines:
+ lines.append(line[offset:])
+ seen_lines.add(lineno)
+ # a non-nested comma separates the expression from the message
+ elif depth == 0 and tp == tokenize.OP and source == ",":
+ # one line assert with message
+ if lineno in seen_lines and len(lines) == 1:
+ offset_in_trimmed = offset + len(lines[-1]) - len(line)
+ lines[-1] = lines[-1][:offset_in_trimmed]
+ # multi-line assert with message
+ elif lineno in seen_lines:
+ lines[-1] = lines[-1][:offset]
+ # multi line assert with escaped newline before message
+ else:
+ lines.append(line[:offset])
+ _write_and_reset()
+ elif tp in {tokenize.NEWLINE, tokenize.ENDMARKER}:
+ _write_and_reset()
+ elif lines and lineno not in seen_lines:
+ lines.append(line)
+ seen_lines.add(lineno)
+
+ return ret
+
+
+class AssertionRewriter(ast.NodeVisitor):
+ """Assertion rewriting implementation.
+
+ The main entrypoint is to call .run() with an ast.Module instance,
+ this will then find all the assert statements and rewrite them to
+ provide intermediate values and a detailed assertion error. See
+ http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
+ for an overview of how this works.
+
+ The entry point here is .run() which will iterate over all the
+ statements in an ast.Module and for each ast.Assert statement it
+ finds call .visit() with it. Then .visit_Assert() takes over and
+ is responsible for creating new ast statements to replace the
+ original assert statement: it rewrites the test of an assertion
+ to provide intermediate values and replace it with an if statement
+ which raises an assertion error with a detailed explanation in
+ case the expression is false and calls pytest_assertion_pass hook
+ if expression is true.
+
+ For this .visit_Assert() uses the visitor pattern to visit all the
+ AST nodes of the ast.Assert.test field, each visit call returning
+ an AST node and the corresponding explanation string. During this
+ state is kept in several instance attributes:
+
+ :statements: All the AST statements which will replace the assert
+ statement.
+
+ :variables: This is populated by .variable() with each variable
+ used by the statements so that they can all be set to None at
+ the end of the statements.
+
+ :variable_counter: Counter to create new unique variables needed
+ by statements. Variables are created using .variable() and
+ have the form of "@py_assert0".
+
+ :expl_stmts: The AST statements which will be executed to get
+ data from the assertion. This is the code which will construct
+ the detailed assertion message that is used in the AssertionError
+ or for the pytest_assertion_pass hook.
+
+ :explanation_specifiers: A dict filled by .explanation_param()
+ with %-formatting placeholders and their corresponding
+ expressions to use in the building of an assertion message.
+ This is used by .pop_format_context() to build a message.
+
+ :stack: A stack of the explanation_specifiers dicts maintained by
+ .push_format_context() and .pop_format_context() which allows
+ to build another %-formatted string while already building one.
+
+ :scope: A tuple containing the current scope used for variables_overwrite.
+
+ :variables_overwrite: A dict filled with references to variables
+ that change value within an assert. This happens when a variable is
+ reassigned with the walrus operator
+
+ This state, except the variables_overwrite, is reset on every new assert
+ statement visited and used by the other visitors.
+ """
+
+ def __init__(
+ self, module_path: str | None, config: Config | None, source: bytes
+ ) -> None:
+ super().__init__()
+ self.module_path = module_path
+ self.config = config
+ if config is not None:
+ self.enable_assertion_pass_hook = config.getini(
+ "enable_assertion_pass_hook"
+ )
+ else:
+ self.enable_assertion_pass_hook = False
+ self.source = source
+ self.scope: tuple[ast.AST, ...] = ()
+ self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = (
+ defaultdict(dict)
+ )
+
+ def run(self, mod: ast.Module) -> None:
+ """Find all assert statements in *mod* and rewrite them."""
+ if not mod.body:
+ # Nothing to do.
+ return
+
+ # We'll insert some special imports at the top of the module, but after any
+ # docstrings and __future__ imports, so first figure out where that is.
+ doc = getattr(mod, "docstring", None)
+ expect_docstring = doc is None
+ if doc is not None and self.is_rewrite_disabled(doc):
+ return
+ pos = 0
+ for item in mod.body:
+ match item:
+ case ast.Expr(value=ast.Constant(value=str() as doc)) if (
+ expect_docstring
+ ):
+ if self.is_rewrite_disabled(doc):
+ return
+ expect_docstring = False
+ case ast.ImportFrom(level=0, module="__future__"):
+ pass
+ case _:
+ break
+ pos += 1
+ # Special case: for a decorated function, set the lineno to that of the
+ # first decorator, not the `def`. Issue #4984.
+ if isinstance(item, ast.FunctionDef) and item.decorator_list:
+ lineno = item.decorator_list[0].lineno
+ else:
+ lineno = item.lineno
+ # Now actually insert the special imports.
+ aliases = [
+ ast.alias("builtins", "@py_builtins", lineno=lineno, col_offset=0),
+ ast.alias(
+ "_pytest.assertion.rewrite",
+ "@pytest_ar",
+ lineno=lineno,
+ col_offset=0,
+ ),
+ ]
+ imports = [
+ ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases
+ ]
+ mod.body[pos:pos] = imports
+
+ # Collect asserts.
+ self.scope = (mod,)
+ nodes: list[ast.AST | Sentinel] = [mod]
+ while nodes:
+ node = nodes.pop()
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef):
+ self.scope = tuple((*self.scope, node))
+ nodes.append(_SCOPE_END_MARKER)
+ if node == _SCOPE_END_MARKER:
+ self.scope = self.scope[:-1]
+ continue
+ assert isinstance(node, ast.AST)
+ for name, field in ast.iter_fields(node):
+ if isinstance(field, list):
+ new: list[ast.AST] = []
+ for i, child in enumerate(field):
+ if isinstance(child, ast.Assert):
+ # Transform assert.
+ new.extend(self.visit(child))
+ else:
+ new.append(child)
+ if isinstance(child, ast.AST):
+ nodes.append(child)
+ setattr(node, name, new)
+ elif (
+ isinstance(field, ast.AST)
+ # Don't recurse into expressions as they can't contain
+ # asserts.
+ and not isinstance(field, ast.expr)
+ ):
+ nodes.append(field)
+
+ @staticmethod
+ def is_rewrite_disabled(docstring: str) -> bool:
+ return "PYTEST_DONT_REWRITE" in docstring
+
+ def variable(self) -> str:
+ """Get a new variable."""
+ # Use a character invalid in python identifiers to avoid clashing.
+ name = "@py_assert" + str(next(self.variable_counter))
+ self.variables.append(name)
+ return name
+
+ def assign(self, expr: ast.expr) -> ast.Name:
+ """Give *expr* a name."""
+ name = self.variable()
+ self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
+ return ast.copy_location(ast.Name(name, ast.Load()), expr)
+
+ def display(self, expr: ast.expr) -> ast.expr:
+ """Call saferepr on the expression."""
+ return self.helper("_saferepr", expr)
+
+ def helper(self, name: str, *args: ast.expr) -> ast.expr:
+ """Call a helper in this module."""
+ py_name = ast.Name("@pytest_ar", ast.Load())
+ attr = ast.Attribute(py_name, name, ast.Load())
+ return ast.Call(attr, list(args), [])
+
+ def builtin(self, name: str) -> ast.Attribute:
+ """Return the builtin called *name*."""
+ builtin_name = ast.Name("@py_builtins", ast.Load())
+ return ast.Attribute(builtin_name, name, ast.Load())
+
+ def explanation_param(self, expr: ast.expr) -> str:
+ """Return a new named %-formatting placeholder for expr.
+
+ This creates a %-formatting placeholder for expr in the
+ current formatting context, e.g. ``%(py0)s``. The placeholder
+ and expr are placed in the current format context so that it
+ can be used on the next call to .pop_format_context().
+ """
+ specifier = "py" + str(next(self.variable_counter))
+ self.explanation_specifiers[specifier] = expr
+ return "%(" + specifier + ")s"
+
+ def push_format_context(self) -> None:
+ """Create a new formatting context.
+
+ The format context is used for when an explanation wants to
+ have a variable value formatted in the assertion message. In
+ this case the value required can be added using
+ .explanation_param(). Finally .pop_format_context() is used
+ to format a string of %-formatted values as added by
+ .explanation_param().
+ """
+ self.explanation_specifiers: dict[str, ast.expr] = {}
+ self.stack.append(self.explanation_specifiers)
+
+ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
+ """Format the %-formatted string with current format context.
+
+ The expl_expr should be an str ast.expr instance constructed from
+ the %-placeholders created by .explanation_param(). This will
+ add the required code to format said string to .expl_stmts and
+ return the ast.Name instance of the formatted string.
+ """
+ current = self.stack.pop()
+ if self.stack:
+ self.explanation_specifiers = self.stack[-1]
+ keys: list[ast.expr | None] = [ast.Constant(key) for key in current.keys()]
+ format_dict = ast.Dict(keys, list(current.values()))
+ form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
+ name = "@py_format" + str(next(self.variable_counter))
+ if self.enable_assertion_pass_hook:
+ self.format_variables.append(name)
+ self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form))
+ return ast.Name(name, ast.Load())
+
+ def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]:
+ """Handle expressions we don't have custom code for."""
+ assert isinstance(node, ast.expr)
+ res = self.assign(node)
+ return res, self.explanation_param(self.display(res))
+
+ def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]:
+ """Return the AST statements to replace the ast.Assert instance.
+
+ This rewrites the test of an assertion to provide
+ intermediate values and replace it with an if statement which
+ raises an assertion error with a detailed explanation in case
+ the expression is false.
+ """
+ if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
+ import warnings
+
+ from _pytest.warning_types import PytestAssertRewriteWarning
+
+ # TODO: This assert should not be needed.
+ assert self.module_path is not None
+ warnings.warn_explicit(
+ PytestAssertRewriteWarning(
+ "assertion is always true, perhaps remove parentheses?"
+ ),
+ category=None,
+ filename=self.module_path,
+ lineno=assert_.lineno,
+ )
+
+ self.statements: list[ast.stmt] = []
+ self.variables: list[str] = []
+ self.variable_counter = itertools.count()
+
+ if self.enable_assertion_pass_hook:
+ self.format_variables: list[str] = []
+
+ self.stack: list[dict[str, ast.expr]] = []
+ self.expl_stmts: list[ast.stmt] = []
+ self.push_format_context()
+ # Rewrite assert into a bunch of statements.
+ top_condition, explanation = self.visit(assert_.test)
+
+ negation = ast.UnaryOp(ast.Not(), top_condition)
+
+ if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
+ msg = self.pop_format_context(ast.Constant(explanation))
+
+ # Failed
+ if assert_.msg:
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
+ gluestr = "\n>assert "
+ else:
+ assertmsg = ast.Constant("")
+ gluestr = "assert "
+ err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
+ err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
+ err_name = ast.Name("AssertionError", ast.Load())
+ fmt = self.helper("_format_explanation", err_msg)
+ exc = ast.Call(err_name, [fmt], [])
+ raise_ = ast.Raise(exc, None)
+ statements_fail = []
+ statements_fail.extend(self.expl_stmts)
+ statements_fail.append(raise_)
+
+ # Passed
+ fmt_pass = self.helper("_format_explanation", msg)
+ orig = _get_assertion_exprs(self.source)[assert_.lineno]
+ hook_call_pass = ast.Expr(
+ self.helper(
+ "_call_assertion_pass",
+ ast.Constant(assert_.lineno),
+ ast.Constant(orig),
+ fmt_pass,
+ )
+ )
+ # If any hooks implement assert_pass hook
+ hook_impl_test = ast.If(
+ self.helper("_check_if_assertion_pass_impl"),
+ [*self.expl_stmts, hook_call_pass],
+ [],
+ )
+ statements_pass: list[ast.stmt] = [hook_impl_test]
+
+ # Test for assertion condition
+ main_test = ast.If(negation, statements_fail, statements_pass)
+ self.statements.append(main_test)
+ if self.format_variables:
+ variables: list[ast.expr] = [
+ ast.Name(name, ast.Store()) for name in self.format_variables
+ ]
+ clear_format = ast.Assign(variables, ast.Constant(None))
+ self.statements.append(clear_format)
+
+ else: # Original assertion rewriting
+ # Create failure message.
+ body = self.expl_stmts
+ self.statements.append(ast.If(negation, body, []))
+ if assert_.msg:
+ assertmsg = self.helper("_format_assertmsg", assert_.msg)
+ explanation = "\n>assert " + explanation
+ else:
+ assertmsg = ast.Constant("")
+ explanation = "assert " + explanation
+ template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
+ msg = self.pop_format_context(template)
+ fmt = self.helper("_format_explanation", msg)
+ err_name = ast.Name("AssertionError", ast.Load())
+ exc = ast.Call(err_name, [fmt], [])
+ raise_ = ast.Raise(exc, None)
+
+ body.append(raise_)
+
+ # Clear temporary variables by setting them to None.
+ if self.variables:
+ variables = [ast.Name(name, ast.Store()) for name in self.variables]
+ clear = ast.Assign(variables, ast.Constant(None))
+ self.statements.append(clear)
+ # Fix locations (line numbers/column offsets).
+ for stmt in self.statements:
+ for node in traverse_node(stmt):
+ if getattr(node, "lineno", None) is None:
+ # apply the assertion location to all generated ast nodes without source location
+ # and preserve the location of existing nodes or generated nodes with an correct location.
+ ast.copy_location(node, assert_)
+ return self.statements
+
+ def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]:
+ # This method handles the 'walrus operator' repr of the target
+ # name if it's a local variable or _should_repr_global_name()
+ # thinks it's acceptable.
+ locs = ast.Call(self.builtin("locals"), [], [])
+ target_id = name.target.id
+ inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
+ dorepr = self.helper("_should_repr_global_name", name)
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
+ expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
+ return name, self.explanation_param(expr)
+
+ def visit_Name(self, name: ast.Name) -> tuple[ast.Name, str]:
+ # Display the repr of the name if it's a local variable or
+ # _should_repr_global_name() thinks it's acceptable.
+ locs = ast.Call(self.builtin("locals"), [], [])
+ inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
+ dorepr = self.helper("_should_repr_global_name", name)
+ test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
+ expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
+ return name, self.explanation_param(expr)
+
+ def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]:
+ res_var = self.variable()
+ expl_list = self.assign(ast.List([], ast.Load()))
+ app = ast.Attribute(expl_list, "append", ast.Load())
+ is_or = int(isinstance(boolop.op, ast.Or))
+ body = save = self.statements
+ fail_save = self.expl_stmts
+ levels = len(boolop.values) - 1
+ self.push_format_context()
+ # Process each operand, short-circuiting if needed.
+ for i, v in enumerate(boolop.values):
+ if i:
+ fail_inner: list[ast.stmt] = []
+ # cond is set in a prior loop iteration below
+ self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
+ self.expl_stmts = fail_inner
+ match v:
+ # Check if the left operand is an ast.NamedExpr and the value has already been visited
+ case ast.Compare(
+ left=ast.NamedExpr(target=ast.Name(id=target_id))
+ ) if target_id in [
+ e.id for e in boolop.values[:i] if hasattr(e, "id")
+ ]:
+ pytest_temp = self.variable()
+ self.variables_overwrite[self.scope][target_id] = v.left # type:ignore[assignment]
+ # mypy's false positive, we're checking that the 'target' attribute exists.
+ v.left.target.id = pytest_temp # type:ignore[attr-defined]
+ self.push_format_context()
+ res, expl = self.visit(v)
+ body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
+ expl_format = self.pop_format_context(ast.Constant(expl))
+ call = ast.Call(app, [expl_format], [])
+ self.expl_stmts.append(ast.Expr(call))
+ if i < levels:
+ cond: ast.expr = res
+ if is_or:
+ cond = ast.UnaryOp(ast.Not(), cond)
+ inner: list[ast.stmt] = []
+ self.statements.append(ast.If(cond, inner, []))
+ self.statements = body = inner
+ self.statements = save
+ self.expl_stmts = fail_save
+ expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
+ expl = self.pop_format_context(expl_template)
+ return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
+
+ def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]:
+ pattern = UNARY_MAP[unary.op.__class__]
+ operand_res, operand_expl = self.visit(unary.operand)
+ res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary))
+ return res, pattern % (operand_expl,)
+
+ def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]:
+ symbol = BINOP_MAP[binop.op.__class__]
+ left_expr, left_expl = self.visit(binop.left)
+ right_expr, right_expl = self.visit(binop.right)
+ explanation = f"({left_expl} {symbol} {right_expl})"
+ res = self.assign(
+ ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop)
+ )
+ return res, explanation
+
+ def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]:
+ new_func, func_expl = self.visit(call.func)
+ arg_expls = []
+ new_args = []
+ new_kwargs = []
+ for arg in call.args:
+ if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
+ self.scope, {}
+ ):
+ arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
+ res, expl = self.visit(arg)
+ arg_expls.append(expl)
+ new_args.append(res)
+ for keyword in call.keywords:
+ match keyword.value:
+ case ast.Name(id=id) if id in self.variables_overwrite.get(
+ self.scope, {}
+ ):
+ keyword.value = self.variables_overwrite[self.scope][id] # type:ignore[assignment]
+ res, expl = self.visit(keyword.value)
+ new_kwargs.append(ast.keyword(keyword.arg, res))
+ if keyword.arg:
+ arg_expls.append(keyword.arg + "=" + expl)
+ else: # **args have `arg` keywords with an .arg of None
+ arg_expls.append("**" + expl)
+
+ expl = "{}({})".format(func_expl, ", ".join(arg_expls))
+ new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call)
+ res = self.assign(new_call)
+ res_expl = self.explanation_param(self.display(res))
+ outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}"
+ return res, outer_expl
+
+ def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]:
+ # A Starred node can appear in a function call.
+ res, expl = self.visit(starred.value)
+ new_starred = ast.Starred(res, starred.ctx)
+ return new_starred, "*" + expl
+
+ def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]:
+ if not isinstance(attr.ctx, ast.Load):
+ return self.generic_visit(attr)
+ value, value_expl = self.visit(attr.value)
+ res = self.assign(
+ ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr)
+ )
+ res_expl = self.explanation_param(self.display(res))
+ pat = "%s\n{%s = %s.%s\n}"
+ expl = pat % (res_expl, res_expl, value_expl, attr.attr)
+ return res, expl
+
+ def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]:
+ self.push_format_context()
+ # We first check if we have overwritten a variable in the previous assert
+ match comp.left:
+ case ast.Name(id=name_id) if name_id in self.variables_overwrite.get(
+ self.scope, {}
+ ):
+ comp.left = self.variables_overwrite[self.scope][name_id] # type: ignore[assignment]
+ case ast.NamedExpr(target=ast.Name(id=target_id)):
+ self.variables_overwrite[self.scope][target_id] = comp.left # type: ignore[assignment]
+ left_res, left_expl = self.visit(comp.left)
+ if isinstance(comp.left, ast.Compare | ast.BoolOp):
+ left_expl = f"({left_expl})"
+ res_variables = [self.variable() for i in range(len(comp.ops))]
+ load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables]
+ store_names = [ast.Name(v, ast.Store()) for v in res_variables]
+ it = zip(range(len(comp.ops)), comp.ops, comp.comparators, strict=True)
+ expls: list[ast.expr] = []
+ syms: list[ast.expr] = []
+ results = [left_res]
+ for i, op, next_operand in it:
+ match (next_operand, left_res):
+ case (
+ ast.NamedExpr(target=ast.Name(id=target_id)),
+ ast.Name(id=name_id),
+ ) if target_id == name_id:
+ next_operand.target.id = self.variable()
+ self.variables_overwrite[self.scope][name_id] = next_operand # type: ignore[assignment]
+
+ next_res, next_expl = self.visit(next_operand)
+ if isinstance(next_operand, ast.Compare | ast.BoolOp):
+ next_expl = f"({next_expl})"
+ results.append(next_res)
+ sym = BINOP_MAP[op.__class__]
+ syms.append(ast.Constant(sym))
+ expl = f"{left_expl} {sym} {next_expl}"
+ expls.append(ast.Constant(expl))
+ res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp)
+ self.statements.append(ast.Assign([store_names[i]], res_expr))
+ left_res, left_expl = next_res, next_expl
+ # Use pytest.assertion.util._reprcompare if that's available.
+ expl_call = self.helper(
+ "_call_reprcompare",
+ ast.Tuple(syms, ast.Load()),
+ ast.Tuple(load_names, ast.Load()),
+ ast.Tuple(expls, ast.Load()),
+ ast.Tuple(results, ast.Load()),
+ )
+ if len(comp.ops) > 1:
+ res: ast.expr = ast.BoolOp(ast.And(), load_names)
+ else:
+ res = load_names[0]
+
+ return res, self.explanation_param(self.pop_format_context(expl_call))
+
+
+def try_makedirs(cache_dir: Path) -> bool:
+ """Attempt to create the given directory and sub-directories exist.
+
+ Returns True if successful or if it already exists.
+ """
+ try:
+ os.makedirs(cache_dir, exist_ok=True)
+ except (FileNotFoundError, NotADirectoryError, FileExistsError):
+ # One of the path components was not a directory:
+ # - we're in a zip file
+ # - it is a file
+ return False
+ except PermissionError:
+ return False
+ except OSError as e:
+ # as of now, EROFS doesn't have an equivalent OSError-subclass
+ #
+ # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not
+ # implemented" for a read-only error
+ if e.errno in {errno.EROFS, errno.ENOSYS}:
+ return False
+ raise
+ return True
+
+
+def get_cache_dir(file_path: Path) -> Path:
+ """Return the cache directory to write .pyc files for the given .py file path."""
+ if sys.pycache_prefix:
+ # given:
+ # prefix = '/tmp/pycs'
+ # path = '/home/user/proj/test_app.py'
+ # we want:
+ # '/tmp/pycs/home/user/proj'
+ return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1])
+ else:
+ # classic pycache directory
+ return file_path.parent / "__pycache__"
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/truncate.py b/venv/lib/python3.11/site-packages/_pytest/assertion/truncate.py
new file mode 100644
index 000000000..d62ca33cc
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/truncate.py
@@ -0,0 +1,128 @@
+"""Utilities for truncating assertion output.
+
+Current default behaviour is to truncate assertion explanations at
+terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
+"""
+
+from __future__ import annotations
+
+from _pytest.compat import running_on_ci
+from _pytest.config import Config
+from _pytest.nodes import Item
+
+
+DEFAULT_MAX_LINES = 8
+DEFAULT_MAX_CHARS = DEFAULT_MAX_LINES * 80
+USAGE_MSG = "use '-vv' to show"
+
+
+def truncate_if_required(explanation: list[str], item: Item) -> list[str]:
+ """Truncate this assertion explanation if the given test item is eligible."""
+ should_truncate, max_lines, max_chars = _get_truncation_parameters(item)
+ if should_truncate:
+ return _truncate_explanation(
+ explanation,
+ max_lines=max_lines,
+ max_chars=max_chars,
+ )
+ return explanation
+
+
+def _get_truncation_parameters(item: Item) -> tuple[bool, int, int]:
+ """Return the truncation parameters related to the given item, as (should truncate, max lines, max chars)."""
+ # We do not need to truncate if one of conditions is met:
+ # 1. Verbosity level is 2 or more;
+ # 2. Test is being run in CI environment;
+ # 3. Both truncation_limit_lines and truncation_limit_chars
+ # .ini parameters are set to 0 explicitly.
+ max_lines = item.config.getini("truncation_limit_lines")
+ max_lines = int(max_lines if max_lines is not None else DEFAULT_MAX_LINES)
+
+ max_chars = item.config.getini("truncation_limit_chars")
+ max_chars = int(max_chars if max_chars is not None else DEFAULT_MAX_CHARS)
+
+ verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
+
+ should_truncate = verbose < 2 and not running_on_ci()
+ should_truncate = should_truncate and (max_lines > 0 or max_chars > 0)
+
+ return should_truncate, max_lines, max_chars
+
+
+def _truncate_explanation(
+ input_lines: list[str],
+ max_lines: int,
+ max_chars: int,
+) -> list[str]:
+ """Truncate given list of strings that makes up the assertion explanation.
+
+ Truncates to either max_lines, or max_chars - whichever the input reaches
+ first, taking the truncation explanation into account. The remaining lines
+ will be replaced by a usage message.
+
+ If max_chars=0, no truncation by character count is performed.
+ If max_lines=0, no truncation by line count is performed.
+
+ When this function is launched we know max_lines > 0 or max_chars > 0
+ because _get_truncation_parameters was called first.
+ """
+ # The length of the truncation explanation depends on the number of lines
+ # removed but is at least 68 characters:
+ # The real value is
+ # 64 (for the base message:
+ # '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
+ # )
+ # + 1 (for plural)
+ # + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
+ # + 3 for the '...' added to the truncated line
+ # But if there's more than 100 lines it's very likely that we're going to
+ # truncate, so we don't need the exact value using log10.
+ tolerable_max_chars = (
+ max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
+ )
+ # The truncation explanation add two lines to the output
+ if max_lines == 0 or len(input_lines) <= max_lines + 2:
+ if max_chars == 0 or sum(len(s) for s in input_lines) <= tolerable_max_chars:
+ return input_lines
+ truncated_explanation = input_lines
+ else:
+ # Truncate first to max_lines, and then truncate to max_chars if necessary
+ truncated_explanation = input_lines[:max_lines]
+ # We reevaluate the need to truncate chars following removal of some lines
+ need_to_truncate_char = (
+ max_chars > 0
+ and sum(len(e) for e in truncated_explanation) > tolerable_max_chars
+ )
+ if need_to_truncate_char:
+ truncated_explanation = _truncate_by_char_count(
+ truncated_explanation, max_chars
+ )
+ # Something was truncated, adding '...' at the end to show that
+ truncated_explanation[-1] += "..."
+ truncated_line_count = (
+ len(input_lines) - len(truncated_explanation) + int(need_to_truncate_char)
+ )
+ return [
+ *truncated_explanation,
+ "",
+ f"...Full output truncated ({truncated_line_count} line"
+ f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
+ ]
+
+
+def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]:
+ # Find point at which input length exceeds total allowed length
+ iterated_char_count = 0
+ for iterated_index, input_line in enumerate(input_lines):
+ if iterated_char_count + len(input_line) > max_chars:
+ break
+ iterated_char_count += len(input_line)
+
+ # Create truncated explanation with modified final line
+ truncated_result = input_lines[:iterated_index]
+ final_line = input_lines[iterated_index]
+ if final_line:
+ final_line_truncate_point = max_chars - iterated_char_count
+ final_line = final_line[:final_line_truncate_point]
+ truncated_result.append(final_line)
+ return truncated_result
diff --git a/venv/lib/python3.11/site-packages/_pytest/assertion/util.py b/venv/lib/python3.11/site-packages/_pytest/assertion/util.py
new file mode 100644
index 000000000..5e5ef543c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/assertion/util.py
@@ -0,0 +1,215 @@
+# mypy: allow-untyped-defs
+"""Utilities for assertion debugging."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from collections.abc import Iterator
+from collections.abc import Sequence
+from typing import Literal
+from unicodedata import normalize
+
+from _pytest import outcomes
+import _pytest._code
+from _pytest._io.saferepr import saferepr
+from _pytest._io.saferepr import saferepr_unlimited
+from _pytest.assertion._compare_any import _compare_eq_any
+from _pytest.assertion._compare_set import SET_COMPARISON_FUNCTIONS
+from _pytest.assertion._guards import isset
+from _pytest.assertion._guards import istext
+from _pytest.assertion._typing import _AssertionTextDiffStyle
+from _pytest.assertion._typing import _HighlightFunc
+from _pytest.assertion.compare_text import _notin_text
+from _pytest.assertion.highlight import dummy_highlighter as dummy_highlighter
+from _pytest.config import Config
+from _pytest.config import UsageError
+
+
+# The _reprcompare attribute on the util module is used by the new assertion
+# interpretation code and assertion rewriter to detect this plugin was
+# loaded and in turn call the hooks defined here as part of the
+# DebugInterpreter.
+_reprcompare: Callable[[str, object, object], str | None] | None = None
+
+# Works similarly as _reprcompare attribute. Is populated with the hook call
+# when pytest_runtest_setup is called.
+_assertion_pass: Callable[[int, str, str], None] | None = None
+
+# Config object which is assigned during pytest_runtest_protocol.
+_config: Config | None = None
+
+ASSERTION_TEXT_DIFF_STYLE_INI = "assertion_text_diff_style"
+ASSERTION_TEXT_DIFF_STYLE_NDIFF: Literal["ndiff"] = "ndiff"
+ASSERTION_TEXT_DIFF_STYLE_BLOCK: Literal["block"] = "block"
+ASSERTION_TEXT_DIFF_STYLE_CHOICES = (
+ ASSERTION_TEXT_DIFF_STYLE_NDIFF,
+ ASSERTION_TEXT_DIFF_STYLE_BLOCK,
+)
+
+
+def get_assertion_text_diff_style(config: Config) -> _AssertionTextDiffStyle:
+ style = str(config.getini(ASSERTION_TEXT_DIFF_STYLE_INI))
+ match style:
+ case "ndiff" | "block":
+ return style
+ case _:
+ choices = ", ".join(
+ repr(choice) for choice in ASSERTION_TEXT_DIFF_STYLE_CHOICES
+ )
+ raise UsageError(
+ f"{ASSERTION_TEXT_DIFF_STYLE_INI} must be one of {choices}; got {style!r}"
+ )
+
+
+def validate_assertion_text_diff_style(config: Config) -> None:
+ get_assertion_text_diff_style(config)
+
+
+def format_explanation(explanation: str) -> str:
+ r"""Format an explanation.
+
+ Normally all embedded newlines are escaped, however there are
+ three exceptions: \n{, \n} and \n~. The first two are intended
+ cover nested explanations, see function and attribute explanations
+ for examples (.visit_Call(), visit_Attribute()). The last one is
+ for when one explanation needs to span multiple lines, e.g. when
+ displaying diffs.
+ """
+ lines = _split_explanation(explanation)
+ result = _format_lines(lines)
+ return "\n".join(result)
+
+
+def _split_explanation(explanation: str) -> list[str]:
+ r"""Return a list of individual lines in the explanation.
+
+ This will return a list of lines split on '\n{', '\n}' and '\n~'.
+ Any other newlines will be escaped and appear in the line as the
+ literal '\n' characters.
+ """
+ raw_lines = (explanation or "").split("\n")
+ lines = [raw_lines[0]]
+ for values in raw_lines[1:]:
+ if values and values[0] in ["{", "}", "~", ">"]:
+ lines.append(values)
+ else:
+ lines[-1] += "\\n" + values
+ return lines
+
+
+def _format_lines(lines: Sequence[str]) -> list[str]:
+ """Format the individual lines.
+
+ This will replace the '{', '}' and '~' characters of our mini formatting
+ language with the proper 'where ...', 'and ...' and ' + ...' text, taking
+ care of indentation along the way.
+
+ Return a list of formatted lines.
+ """
+ result = list(lines[:1])
+ stack = [0]
+ stackcnt = [0]
+ for line in lines[1:]:
+ if line.startswith("{"):
+ if stackcnt[-1]:
+ s = "and "
+ else:
+ s = "where "
+ stack.append(len(result))
+ stackcnt[-1] += 1
+ stackcnt.append(0)
+ result.append(" +" + " " * (len(stack) - 1) + s + line[1:])
+ elif line.startswith("}"):
+ stack.pop()
+ stackcnt.pop()
+ result[stack[-1]] += line[1:]
+ else:
+ assert line[0] in ["~", ">"]
+ stack[-1] += 1
+ indent = len(stack) if line.startswith("~") else len(stack) - 1
+ result.append(" " * indent + line[1:])
+ assert len(stack) == 1
+ return result
+
+
+def assertrepr_compare(
+ op: str,
+ left: object,
+ right: object,
+ *,
+ verbose: int,
+ highlighter: _HighlightFunc,
+ assertion_text_diff_style: _AssertionTextDiffStyle,
+) -> Iterator[str]:
+ """Yield specialised explanations for some operators/operands.
+
+ The first line yielded is always the summary (``left op right``);
+ subsequent lines are the detailed explanation. Yields nothing when no
+ specialised explanation applies, which lets consumers map an empty
+ iterator to "no explanation" without materialising anything.
+
+ The iterator is lazy on purpose: a streaming consumer can stop pulling
+ lines as soon as it has enough to show, so an enormous diff doesn't
+ have to be built in full just to be thrown away.
+ """
+ # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
+ # See issue #3246.
+ use_ascii = (
+ isinstance(left, str)
+ and isinstance(right, str)
+ and normalize("NFD", left) == normalize("NFD", right)
+ )
+
+ if verbose > 1:
+ left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
+ right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
+ else:
+ # XXX: "15 chars indentation" is wrong
+ # ("E AssertionError: assert "); should use term width.
+ maxsize = (
+ 80 - 15 - len(op) - 2
+ ) // 2 # 15 chars indentation, 1 space around op
+
+ left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
+ right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
+
+ summary = f"{left_repr} {op} {right_repr}"
+
+ try:
+ if op == "==":
+ source = _compare_eq_any(
+ left,
+ right,
+ highlighter,
+ verbose,
+ assertion_text_diff_style,
+ )
+ elif op == "not in" and istext(left) and istext(right):
+ source = _notin_text(left, right, verbose)
+ elif op in {"!=", ">=", "<=", ">", "<"} and isset(left) and isset(right):
+ source = SET_COMPARISON_FUNCTIONS[op](left, right, highlighter, verbose)
+ else:
+ source = iter(())
+
+ # Only yield the summary if there is a detailed explanation.
+ # Make sure there's a separating empty line after the summary.
+ summary_yielded = False
+ for line in source:
+ if not summary_yielded:
+ yield summary
+ if line != "":
+ yield ""
+ summary_yielded = True
+ yield line
+ except outcomes.Exit:
+ raise
+ except Exception:
+ repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
+ if not summary_yielded:
+ yield summary
+ yield ""
+ summary_yielded = True
+ yield (
+ f"(pytest_assertion plugin: representation of details failed: {repr_crash}."
+ )
+ yield " Probably an object has a faulty __repr__.)"
diff --git a/venv/lib/python3.11/site-packages/_pytest/cacheprovider.py b/venv/lib/python3.11/site-packages/_pytest/cacheprovider.py
new file mode 100644
index 000000000..6bcac1ad9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/cacheprovider.py
@@ -0,0 +1,640 @@
+# mypy: allow-untyped-defs
+"""Implementation of the cache provider."""
+
+# This plugin was not named "cache" to avoid conflicts with the external
+# pytest-cache version.
+from __future__ import annotations
+
+from collections.abc import Generator
+from collections.abc import Iterable
+import dataclasses
+import errno
+import json
+import os
+from pathlib import Path
+import shutil
+import tempfile
+from typing import final
+
+from .pathlib import resolve_from_str
+from .pathlib import rm_rf
+from .reports import CollectReport
+from _pytest import nodes
+from _pytest._io import TerminalWriter
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.nodes import Directory
+from _pytest.nodes import File
+from _pytest.reports import TestReport
+
+
+CACHEDIR_FILES: dict[str, bytes] = {
+ "README.md": b"""\
+# pytest cache directory #
+
+This directory contains data from the pytest's cache plugin,
+which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
+
+**Do not** commit this to version control.
+
+See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
+""",
+ ".gitignore": b"# Created by pytest automatically.\n*\n",
+ "CACHEDIR.TAG": b"""\
+Signature: 8a477f597d28d172789f06886806bc55
+# This file is a cache directory tag created by pytest.
+# For information about cache directory tags, see:
+# https://bford.info/cachedir/spec.html
+""",
+}
+
+
+def _make_cachedir(target: Path) -> None:
+ """Create the pytest cache directory atomically with supporting files.
+
+ Creates a temporary directory with README.md, .gitignore, and CACHEDIR.TAG,
+ then atomically renames it to the target location. If another process wins
+ the race, the temporary directory is cleaned up.
+ """
+ target.parent.mkdir(parents=True, exist_ok=True)
+ path = Path(tempfile.mkdtemp(prefix="pytest-cache-files-", dir=target.parent))
+ try:
+ # Reset permissions to the default, see #12308.
+ # Note: there's no way to get the current umask atomically, eek.
+ umask = os.umask(0o022)
+ os.umask(umask)
+ path.chmod(0o777 - umask)
+
+ for name, content in CACHEDIR_FILES.items():
+ path.joinpath(name).write_bytes(content)
+
+ path.rename(target)
+ except OSError as e:
+ # If 2 concurrent pytests both race to the rename, the loser
+ # gets "Directory not empty" from the rename. In this case,
+ # everything is handled so just continue after cleanup.
+ # On Windows, the error is a FileExistsError which translates to EEXIST.
+ if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
+ raise
+ finally:
+ shutil.rmtree(path, ignore_errors=True)
+
+
+@final
+@dataclasses.dataclass
+class Cache:
+ """Instance of the `cache` fixture."""
+
+ _cachedir: Path = dataclasses.field(repr=False)
+ _config: Config = dataclasses.field(repr=False)
+
+ # Sub-directory under cache-dir for directories created by `mkdir()`.
+ _CACHE_PREFIX_DIRS = "d"
+
+ # Sub-directory under cache-dir for values created by `set()`.
+ _CACHE_PREFIX_VALUES = "v"
+
+ def __init__(
+ self, cachedir: Path, config: Config, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._cachedir = cachedir
+ self._config = config
+
+ @classmethod
+ def for_config(cls, config: Config, *, _ispytest: bool = False) -> Cache:
+ """Create the Cache instance for a Config.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ cachedir = cls.cache_dir_from_config(config, _ispytest=True)
+ if config.getoption("cacheclear") and cachedir.is_dir():
+ cls.clear_cache(cachedir, _ispytest=True)
+ return cls(cachedir, config, _ispytest=True)
+
+ @classmethod
+ def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
+ """Clear the sub-directories used to hold cached directories and values.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
+ d = cachedir / prefix
+ if d.is_dir():
+ rm_rf(d)
+
+ @staticmethod
+ def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
+ """Get the path to the cache directory for a Config.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ return resolve_from_str(config.getini("cache_dir"), config.rootpath)
+
+ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
+ """Issue a cache warning.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ import warnings
+
+ from _pytest.warning_types import PytestCacheWarning
+
+ warnings.warn(
+ PytestCacheWarning(fmt.format(**args) if args else fmt),
+ self._config.hook,
+ stacklevel=3,
+ )
+
+ def _mkdir(self, path: Path) -> None:
+ self._ensure_cache_dir_and_supporting_files()
+ path.mkdir(exist_ok=True, parents=True)
+
+ def mkdir(self, name: str) -> Path:
+ """Return a directory path object with the given name.
+
+ If the directory does not yet exist, it will be created. You can use
+ it to manage files to e.g. store/retrieve database dumps across test
+ sessions.
+
+ .. versionadded:: 7.0
+
+ :param name:
+ Must be a string not containing a ``/`` separator.
+ Make sure the name contains your plugin or application
+ identifiers to prevent clashes with other cache users.
+ """
+ path = Path(name)
+ if len(path.parts) > 1:
+ raise ValueError("name is not allowed to contain path separators")
+ res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
+ self._mkdir(res)
+ return res
+
+ def _getvaluepath(self, key: str) -> Path:
+ return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key))
+
+ def get(self, key: str, default):
+ """Return the cached value for the given key.
+
+ If no value was yet cached or the value cannot be read, the specified
+ default is returned.
+
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param default:
+ The value to return in case of a cache-miss or invalid cache value.
+ """
+ path = self._getvaluepath(key)
+ try:
+ with path.open("r", encoding="UTF-8") as f:
+ return json.load(f)
+ except (ValueError, OSError):
+ return default
+
+ def set(self, key: str, value: object) -> None:
+ """Save value for the given key.
+
+ :param key:
+ Must be a ``/`` separated value. Usually the first
+ name is the name of your plugin or your application.
+ :param value:
+ Must be of any combination of basic python types,
+ including nested types like lists of dictionaries.
+ """
+ path = self._getvaluepath(key)
+ try:
+ self._mkdir(path.parent)
+ except OSError as exc:
+ self.warn(
+ f"could not create cache path {path}: {exc}",
+ _ispytest=True,
+ )
+ return
+ data = json.dumps(value, ensure_ascii=False, indent=2)
+ try:
+ f = path.open("w", encoding="UTF-8")
+ except OSError as exc:
+ self.warn(
+ f"cache could not write path {path}: {exc}",
+ _ispytest=True,
+ )
+ else:
+ with f:
+ f.write(data)
+
+ def _ensure_cache_dir_and_supporting_files(self) -> None:
+ """Create the cache dir and its supporting files."""
+ if not self._cachedir.is_dir():
+ _make_cachedir(self._cachedir)
+
+
+class LFPluginCollWrapper:
+ def __init__(self, lfplugin: LFPlugin) -> None:
+ self.lfplugin = lfplugin
+ self._collected_at_least_one_failure = False
+
+ @hookimpl(wrapper=True)
+ def pytest_make_collect_report(
+ self, collector: nodes.Collector
+ ) -> Generator[None, CollectReport, CollectReport]:
+ res = yield
+ if isinstance(collector, Session | Directory):
+ # Sort any lf-paths to the beginning.
+ lf_paths = self.lfplugin._last_failed_paths
+
+ # Use stable sort to prioritize last failed.
+ def sort_key(node: nodes.Item | nodes.Collector) -> bool:
+ return node.path in lf_paths
+
+ res.result = sorted(
+ res.result,
+ key=sort_key,
+ reverse=True,
+ )
+
+ elif isinstance(collector, File):
+ if collector.path in self.lfplugin._last_failed_paths:
+ result = res.result
+ lastfailed = self.lfplugin.lastfailed
+
+ # Only filter with known failures.
+ if not self._collected_at_least_one_failure:
+ if not any(x.nodeid in lastfailed for x in result):
+ return res
+ self.lfplugin.config.pluginmanager.register(
+ LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
+ )
+ self._collected_at_least_one_failure = True
+
+ session = collector.session
+ result[:] = [
+ x
+ for x in result
+ if x.nodeid in lastfailed
+ # Include any passed arguments (not trivial to filter).
+ or session.isinitpath(x.path)
+ # Keep all sub-collectors.
+ or isinstance(x, nodes.Collector)
+ ]
+
+ return res
+
+
+class LFPluginCollSkipfiles:
+ def __init__(self, lfplugin: LFPlugin) -> None:
+ self.lfplugin = lfplugin
+
+ @hookimpl
+ def pytest_make_collect_report(
+ self, collector: nodes.Collector
+ ) -> CollectReport | None:
+ if isinstance(collector, File):
+ if collector.path not in self.lfplugin._last_failed_paths:
+ self.lfplugin._skipped_files += 1
+
+ return CollectReport(
+ collector.nodeid, "passed", longrepr=None, result=[]
+ )
+ return None
+
+
+class LFPlugin:
+ """Plugin which implements the --lf (run last-failing) option."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ active_keys = "lf", "failedfirst"
+ self.active = any(config.getoption(key) for key in active_keys)
+ assert config.cache
+ self.lastfailed: dict[str, bool] = config.cache.get("cache/lastfailed", {})
+ self._previously_failed_count: int | None = None
+ self._report_status: str | None = None
+ self._skipped_files = 0 # count skipped files during collection due to --lf
+
+ if config.getoption("lf"):
+ self._last_failed_paths = self.get_last_failed_paths()
+ config.pluginmanager.register(
+ LFPluginCollWrapper(self), "lfplugin-collwrapper"
+ )
+
+ def get_last_failed_paths(self) -> set[Path]:
+ """Return a set with all Paths of the previously failed nodeids and
+ their parents."""
+ rootpath = self.config.rootpath
+ result = set()
+ for nodeid in self.lastfailed:
+ path = rootpath / nodeid.split("::")[0]
+ result.add(path)
+ result.update(path.parents)
+ return {x for x in result if x.exists()}
+
+ def pytest_report_collectionfinish(self) -> str | None:
+ if self.active and self.config.get_verbosity() >= 0:
+ return f"run-last-failure: {self._report_status}"
+ return None
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ if (report.when == "call" and report.passed) or report.skipped:
+ self.lastfailed.pop(report.nodeid, None)
+ elif report.failed:
+ self.lastfailed[report.nodeid] = True
+
+ def pytest_collectreport(self, report: CollectReport) -> None:
+ passed = report.outcome in ("passed", "skipped")
+ if passed:
+ if report.nodeid in self.lastfailed:
+ self.lastfailed.pop(report.nodeid)
+ self.lastfailed.update((item.nodeid, True) for item in report.result)
+ else:
+ self.lastfailed[report.nodeid] = True
+
+ @hookimpl(wrapper=True, tryfirst=True)
+ def pytest_collection_modifyitems(
+ self, config: Config, items: list[nodes.Item]
+ ) -> Generator[None]:
+ res = yield
+
+ if not self.active:
+ return res
+
+ if self.lastfailed:
+ previously_failed = []
+ previously_passed = []
+ for item in items:
+ if item.nodeid in self.lastfailed:
+ previously_failed.append(item)
+ else:
+ previously_passed.append(item)
+ self._previously_failed_count = len(previously_failed)
+
+ if not previously_failed:
+ # Running a subset of all tests with recorded failures
+ # only outside of it.
+ self._report_status = (
+ f"{len(self.lastfailed)} known failures not in selected tests"
+ )
+ else:
+ if self.config.getoption("lf"):
+ items[:] = previously_failed
+ config.hook.pytest_deselected(items=previously_passed)
+ else: # --failedfirst
+ items[:] = previously_failed + previously_passed
+
+ noun = "failure" if self._previously_failed_count == 1 else "failures"
+ suffix = " first" if self.config.getoption("failedfirst") else ""
+ self._report_status = (
+ f"rerun previous {self._previously_failed_count} {noun}{suffix}"
+ )
+
+ if self._skipped_files > 0:
+ files_noun = "file" if self._skipped_files == 1 else "files"
+ self._report_status += f" (skipped {self._skipped_files} {files_noun})"
+ else:
+ self._report_status = "no previously failed tests, "
+ if self.config.getoption("last_failed_no_failures") == "none":
+ self._report_status += "deselecting all items."
+ config.hook.pytest_deselected(items=items[:])
+ items[:] = []
+ else:
+ self._report_status += "not deselecting items."
+
+ return res
+
+ def pytest_sessionfinish(self, session: Session) -> None:
+ config = self.config
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
+ return
+
+ assert config.cache is not None
+ saved_lastfailed = config.cache.get("cache/lastfailed", {})
+ if saved_lastfailed != self.lastfailed:
+ config.cache.set("cache/lastfailed", self.lastfailed)
+
+
+class NFPlugin:
+ """Plugin which implements the --nf (run new-first) option."""
+
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ self.active = config.option.newfirst
+ assert config.cache is not None
+ self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
+
+ @hookimpl(wrapper=True, tryfirst=True)
+ def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> Generator[None]:
+ res = yield
+
+ if self.active:
+ new_items: dict[str, nodes.Item] = {}
+ other_items: dict[str, nodes.Item] = {}
+ for item in items:
+ if item.nodeid not in self.cached_nodeids:
+ new_items[item.nodeid] = item
+ else:
+ other_items[item.nodeid] = item
+
+ items[:] = self._get_increasing_order(
+ new_items.values()
+ ) + self._get_increasing_order(other_items.values())
+ self.cached_nodeids.update(new_items)
+ else:
+ self.cached_nodeids.update(item.nodeid for item in items)
+
+ return res
+
+ def _get_increasing_order(self, items: Iterable[nodes.Item]) -> list[nodes.Item]:
+ return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
+
+ def pytest_sessionfinish(self) -> None:
+ config = self.config
+ if config.getoption("cacheshow") or hasattr(config, "workerinput"):
+ return
+
+ if config.getoption("collectonly"):
+ return
+
+ assert config.cache is not None
+ config.cache.set("cache/nodeids", sorted(self.cached_nodeids))
+
+
+def pytest_addoption(parser: Parser) -> None:
+ """Add command-line options for cache functionality.
+
+ :param parser: Parser object to add command-line options to.
+ """
+ group = parser.getgroup("general")
+ group.addoption(
+ "--lf",
+ "--last-failed",
+ action="store_true",
+ dest="lf",
+ help="Rerun only the tests that failed at the last run (or all if none failed)",
+ )
+ group.addoption(
+ "--ff",
+ "--failed-first",
+ action="store_true",
+ dest="failedfirst",
+ help="Run all tests, but run the last failures first. "
+ "This may re-order tests and thus lead to "
+ "repeated fixture setup/teardown.",
+ )
+ group.addoption(
+ "--nf",
+ "--new-first",
+ action="store_true",
+ dest="newfirst",
+ help="Run tests from new files first, then the rest of the tests "
+ "sorted by file mtime",
+ )
+ group.addoption(
+ "--cache-show",
+ action="append",
+ nargs="?",
+ dest="cacheshow",
+ help=(
+ "Show cache contents, don't perform collection or tests. "
+ "Optional argument: glob (default: '*')."
+ ),
+ )
+ group.addoption(
+ "--cache-clear",
+ action="store_true",
+ dest="cacheclear",
+ help="Remove all cache contents at start of test run",
+ )
+ cache_dir_default = ".pytest_cache"
+ if "TOX_ENV_DIR" in os.environ:
+ cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default)
+ parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path")
+ group.addoption(
+ "--lfnf",
+ "--last-failed-no-failures",
+ action="store",
+ dest="last_failed_no_failures",
+ choices=("all", "none"),
+ default="all",
+ help="With ``--lf``, determines whether to execute tests when there "
+ "are no previously (known) failures or when no "
+ "cached ``lastfailed`` data was found. "
+ "``all`` (the default) runs the full test suite again. "
+ "``none`` just emits a message about no known failures and exits successfully.",
+ )
+
+
+def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
+ if config.option.cacheshow and not config.option.help:
+ from _pytest.main import wrap_session
+
+ return wrap_session(config, cacheshow)
+ return None
+
+
+@hookimpl(tryfirst=True)
+def pytest_configure(config: Config) -> None:
+ """Configure cache system and register related plugins.
+
+ Creates the Cache instance and registers the last-failed (LFPlugin)
+ and new-first (NFPlugin) plugins with the plugin manager.
+
+ :param config: pytest configuration object.
+ """
+ config.cache = Cache.for_config(config, _ispytest=True)
+ config.pluginmanager.register(LFPlugin(config), "lfplugin")
+ config.pluginmanager.register(NFPlugin(config), "nfplugin")
+
+
+@fixture
+def cache(request: FixtureRequest) -> Cache:
+ """Return a cache object that can persist state between testing sessions.
+
+ cache.get(key, default)
+ cache.set(key, value)
+
+ Keys must be ``/`` separated strings, where the first part is usually the
+ name of your plugin or application to avoid clashes with other cache users.
+
+ Values can be any object handled by the json stdlib module.
+ """
+ assert request.config.cache is not None
+ return request.config.cache
+
+
+def pytest_report_header(config: Config) -> str | None:
+ """Display cachedir with --cache-show and if non-default."""
+ if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache":
+ assert config.cache is not None
+ cachedir = config.cache._cachedir
+ # TODO: evaluate generating upward relative paths
+ # starting with .., ../.. if sensible
+
+ try:
+ displaypath = cachedir.relative_to(config.rootpath)
+ except ValueError:
+ displaypath = cachedir
+ return f"cachedir: {displaypath}"
+ return None
+
+
+def cacheshow(config: Config, session: Session) -> int:
+ """Display cache contents when --cache-show is used.
+
+ Shows cached values and directories matching the specified glob pattern
+ (default: '*'). Displays cache location, cached test results, and
+ any cached directories created by plugins.
+
+ :param config: pytest configuration object.
+ :param session: pytest session object.
+ :returns: Exit code (0 for success).
+ """
+ from pprint import pformat
+
+ assert config.cache is not None
+
+ tw = TerminalWriter()
+ tw.line("cachedir: " + str(config.cache._cachedir))
+ if not config.cache._cachedir.is_dir():
+ tw.line("cache is empty")
+ return 0
+
+ glob = config.option.cacheshow[0]
+ if glob is None:
+ glob = "*"
+
+ dummy = object()
+ basedir = config.cache._cachedir
+ vdir = basedir / Cache._CACHE_PREFIX_VALUES
+ tw.sep("-", f"cache values for {glob!r}")
+ for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()):
+ key = str(valpath.relative_to(vdir))
+ val = config.cache.get(key, dummy)
+ if val is dummy:
+ tw.line(f"{key} contains unreadable content, will be ignored")
+ else:
+ tw.line(f"{key} contains:")
+ for line in pformat(val).splitlines():
+ tw.line(" " + line)
+
+ ddir = basedir / Cache._CACHE_PREFIX_DIRS
+ if ddir.is_dir():
+ contents = sorted(ddir.rglob(glob))
+ tw.sep("-", f"cache directories for {glob!r}")
+ for p in contents:
+ # if p.is_dir():
+ # print("%s/" % p.relative_to(basedir))
+ if p.is_file():
+ key = str(p.relative_to(basedir))
+ tw.line(f"{key} is a file of length {p.stat().st_size}")
+ return 0
diff --git a/venv/lib/python3.11/site-packages/_pytest/capture.py b/venv/lib/python3.11/site-packages/_pytest/capture.py
new file mode 100644
index 000000000..f9f8f9e4a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/capture.py
@@ -0,0 +1,1151 @@
+# mypy: allow-untyped-defs
+"""Per-test stdout/stderr capturing mechanism."""
+
+from __future__ import annotations
+
+import abc
+import collections
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Iterator
+import contextlib
+import io
+from io import UnsupportedOperation
+import os
+import sys
+from tempfile import TemporaryFile
+from types import TracebackType
+from typing import Any
+from typing import AnyStr
+from typing import BinaryIO
+from typing import cast
+from typing import Final
+from typing import final
+from typing import Generic
+from typing import Literal
+from typing import NamedTuple
+from typing import TextIO
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import SubRequest
+from _pytest.nodes import Collector
+from _pytest.nodes import File
+from _pytest.nodes import Item
+from _pytest.reports import CollectReport
+
+
+_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--capture",
+ action="store",
+ default="fd",
+ metavar="method",
+ choices=["fd", "sys", "no", "tee-sys"],
+ help="Per-test capturing method: one of fd|sys|no|tee-sys",
+ )
+ group._addoption( # private to use reserved lower-case short option
+ "-s",
+ action="store_const",
+ const="no",
+ dest="capture",
+ help="Shortcut for --capture=no",
+ )
+
+
+def _colorama_workaround() -> None:
+ """Ensure colorama is imported so that it attaches to the correct stdio
+ handles on Windows.
+
+ colorama uses the terminal on import time. So if something does the
+ first import of colorama while I/O capture is active, colorama will
+ fail in various ways.
+ """
+ if sys.platform.startswith("win32"):
+ try:
+ import colorama # noqa: F401
+ except ImportError:
+ pass
+
+
+def _readline_workaround() -> None:
+ """Ensure readline is imported early so it attaches to the correct stdio handles.
+
+ This isn't a problem with the default GNU readline implementation, but in
+ some configurations, Python uses libedit instead (on macOS, and for prebuilt
+ binaries such as used by uv).
+
+ In theory this is only needed if readline.backend == "libedit", but the
+ workaround consists of importing readline here, so we already worked around
+ the issue by the time we could check if we need to.
+ """
+ try:
+ import readline # noqa: F401
+ except ImportError:
+ pass
+
+
+def _windowsconsoleio_workaround(stream: TextIO) -> None:
+ """Workaround for Windows Unicode console handling.
+
+ Python 3.6 implemented Unicode console handling for Windows. This works
+ by reading/writing to the raw console handle using
+ ``{Read,Write}ConsoleW``.
+
+ The problem is that we are going to ``dup2`` over the stdio file
+ descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
+ handles used by Python to write to the console. Though there is still some
+ weirdness and the console handle seems to only be closed randomly and not
+ on the first call to ``CloseHandle``, or maybe it gets reopened with the
+ same handle value when we suspend capturing.
+
+ The workaround in this case will reopen stdio with a different fd which
+ also means a different handle by replicating the logic in
+ "Py_lifecycle.c:initstdio/create_stdio".
+
+ :param stream:
+ In practice ``sys.stdout`` or ``sys.stderr``, but given
+ here as parameter for unittesting purposes.
+
+ See https://github.com/pytest-dev/py/issues/103.
+ """
+ if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
+ return
+
+ # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
+ if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
+ return
+
+ raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
+
+ if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
+ return
+
+ def _reopen_stdio(f, mode):
+ if not hasattr(stream.buffer, "raw") and mode[0] == "w":
+ buffering = 0
+ else:
+ buffering = -1
+
+ return io.TextIOWrapper(
+ open(os.dup(f.fileno()), mode, buffering),
+ f.encoding,
+ f.errors,
+ f.newlines,
+ f.line_buffering,
+ )
+
+ sys.stdin = _reopen_stdio(sys.stdin, "rb")
+ sys.stdout = _reopen_stdio(sys.stdout, "wb")
+ sys.stderr = _reopen_stdio(sys.stderr, "wb")
+
+
+@hookimpl(wrapper=True)
+def pytest_load_initial_conftests(early_config: Config) -> Generator[None]:
+ ns = early_config.known_args_namespace
+ if ns.capture == "fd":
+ _windowsconsoleio_workaround(sys.stdout)
+ _colorama_workaround()
+ _readline_workaround()
+ pluginmanager = early_config.pluginmanager
+ capman = CaptureManager(ns.capture)
+ pluginmanager.register(capman, "capturemanager")
+
+ # Make sure that capturemanager is properly reset at final shutdown.
+ early_config.add_cleanup(capman.stop_global_capturing)
+
+ # Finally trigger conftest loading but while capturing (issue #93).
+ capman.start_global_capturing()
+ try:
+ try:
+ yield
+ finally:
+ capman.suspend_global_capture()
+ except BaseException:
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+ raise
+
+
+# IO Helpers.
+
+
+class EncodedFile(io.TextIOWrapper):
+ __slots__ = ()
+
+ @property
+ def name(self) -> str:
+ # Ensure that file.name is a string. Workaround for a Python bug
+ # fixed in >=3.7.4: https://bugs.python.org/issue36015
+ return repr(self.buffer)
+
+ @property
+ def mode(self) -> str:
+ # TextIOWrapper doesn't expose a mode, but at least some of our
+ # tests check it.
+ assert hasattr(self.buffer, "mode")
+ return cast(str, self.buffer.mode.replace("b", ""))
+
+
+class CaptureIO(io.TextIOWrapper):
+ def __init__(self) -> None:
+ super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
+
+ def getvalue(self) -> str:
+ assert isinstance(self.buffer, io.BytesIO)
+ return self.buffer.getvalue().decode("UTF-8")
+
+
+class TeeCaptureIO(CaptureIO):
+ def __init__(self, other: TextIO) -> None:
+ self._other = other
+ super().__init__()
+
+ def write(self, s: str) -> int:
+ super().write(s)
+ return self._other.write(s)
+
+
+class DontReadFromInput(TextIO):
+ @property
+ def encoding(self) -> str:
+ assert sys.__stdin__ is not None
+ return sys.__stdin__.encoding
+
+ def read(self, size: int = -1) -> str:
+ raise OSError(
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
+ )
+
+ readline = read
+
+ def __next__(self) -> str:
+ return self.readline()
+
+ def readlines(self, hint: int | None = -1) -> list[str]:
+ raise OSError(
+ "pytest: reading from stdin while output is captured! Consider using `-s`."
+ )
+
+ def __iter__(self) -> Iterator[str]:
+ return self
+
+ def fileno(self) -> int:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
+
+ def flush(self) -> None:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
+
+ def isatty(self) -> bool:
+ return False
+
+ def close(self) -> None:
+ pass
+
+ def readable(self) -> bool:
+ return False
+
+ def seek(self, offset: int, whence: int = 0) -> int:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
+
+ def seekable(self) -> bool:
+ return False
+
+ def tell(self) -> int:
+ raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
+
+ def truncate(self, size: int | None = None) -> int:
+ raise UnsupportedOperation("cannot truncate stdin")
+
+ def write(self, data: str) -> int:
+ raise UnsupportedOperation("cannot write to stdin")
+
+ def writelines(self, lines: Iterable[str]) -> None:
+ raise UnsupportedOperation("Cannot write to stdin")
+
+ def writable(self) -> bool:
+ return False
+
+ def __enter__(self) -> Self:
+ return self
+
+ def __exit__(
+ self,
+ type: type[BaseException] | None,
+ value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ pass
+
+ @property
+ def buffer(self) -> BinaryIO:
+ # The str/bytes doesn't actually matter in this type, so OK to fake.
+ return self # type: ignore[return-value]
+
+
+# Capture classes.
+
+
+class CaptureBase(abc.ABC, Generic[AnyStr]):
+ EMPTY_BUFFER: AnyStr
+
+ @abc.abstractmethod
+ def __init__(self, fd: int) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def start(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def done(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def suspend(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def resume(self) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def writeorg(self, data: AnyStr) -> None:
+ raise NotImplementedError()
+
+ @abc.abstractmethod
+ def snap(self) -> AnyStr:
+ raise NotImplementedError()
+
+
+patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
+
+
+class NoCapture(CaptureBase[str]):
+ EMPTY_BUFFER = ""
+
+ def __init__(self, fd: int) -> None:
+ pass
+
+ def start(self) -> None:
+ pass
+
+ def done(self) -> None:
+ pass
+
+ def suspend(self) -> None:
+ pass
+
+ def resume(self) -> None:
+ pass
+
+ def snap(self) -> str:
+ return ""
+
+ def writeorg(self, data: str) -> None:
+ pass
+
+
+class SysCaptureBase(CaptureBase[AnyStr]):
+ def __init__(
+ self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False
+ ) -> None:
+ name = patchsysdict[fd]
+ self._old: TextIO = getattr(sys, name)
+ self.name = name
+ if tmpfile is None:
+ if name == "stdin":
+ tmpfile = DontReadFromInput()
+ else:
+ tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)
+ self.tmpfile = tmpfile
+ self._state = "initialized"
+
+ def repr(self, class_name: str) -> str:
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
+ class_name,
+ self.name,
+ (hasattr(self, "_old") and repr(self._old)) or "",
+ self._state,
+ self.tmpfile,
+ )
+
+ def __repr__(self) -> str:
+ return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
+ self.__class__.__name__,
+ self.name,
+ (hasattr(self, "_old") and repr(self._old)) or "",
+ self._state,
+ self.tmpfile,
+ )
+
+ def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
+ assert self._state in states, (
+ "cannot {} in state {!r}: expected one of {}".format(
+ op, self._state, ", ".join(states)
+ )
+ )
+
+ def start(self) -> None:
+ self._assert_state("start", ("initialized",))
+ setattr(sys, self.name, self.tmpfile)
+ self._state = "started"
+
+ def done(self) -> None:
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
+ if self._state == "done":
+ return
+ setattr(sys, self.name, self._old)
+ del self._old
+ self.tmpfile.close()
+ self._state = "done"
+
+ def suspend(self) -> None:
+ self._assert_state("suspend", ("started", "suspended"))
+ setattr(sys, self.name, self._old)
+ self._state = "suspended"
+
+ def resume(self) -> None:
+ self._assert_state("resume", ("started", "suspended"))
+ if self._state == "started":
+ return
+ setattr(sys, self.name, self.tmpfile)
+ self._state = "started"
+
+
+class SysCaptureBinary(SysCaptureBase[bytes]):
+ EMPTY_BUFFER = b""
+
+ def snap(self) -> bytes:
+ self._assert_state("snap", ("started", "suspended"))
+ self.tmpfile.seek(0)
+ res = self.tmpfile.buffer.read()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data: bytes) -> None:
+ self._assert_state("writeorg", ("started", "suspended"))
+ self._old.flush()
+ self._old.buffer.write(data)
+ self._old.buffer.flush()
+
+
+class SysCapture(SysCaptureBase[str]):
+ EMPTY_BUFFER = ""
+
+ def snap(self) -> str:
+ self._assert_state("snap", ("started", "suspended"))
+ assert isinstance(self.tmpfile, CaptureIO)
+ res = self.tmpfile.getvalue()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data: str) -> None:
+ self._assert_state("writeorg", ("started", "suspended"))
+ self._old.write(data)
+ self._old.flush()
+
+
+class FDCaptureBase(CaptureBase[AnyStr]):
+ def __init__(self, targetfd: int) -> None:
+ self.targetfd = targetfd
+
+ try:
+ os.fstat(targetfd)
+ except OSError:
+ # FD capturing is conceptually simple -- create a temporary file,
+ # redirect the FD to it, redirect back when done. But when the
+ # target FD is invalid it throws a wrench into this lovely scheme.
+ #
+ # Tests themselves shouldn't care if the FD is valid, FD capturing
+ # should work regardless of external circumstances. So falling back
+ # to just sys capturing is not a good option.
+ #
+ # Further complications are the need to support suspend() and the
+ # possibility of FD reuse (e.g. the tmpfile getting the very same
+ # target FD). The following approach is robust, I believe.
+ self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR)
+ os.dup2(self.targetfd_invalid, targetfd)
+ else:
+ self.targetfd_invalid = None
+ self.targetfd_save = os.dup(targetfd)
+
+ if targetfd == 0:
+ self.tmpfile = open(os.devnull, encoding="utf-8")
+ self.syscapture: CaptureBase[str] = SysCapture(targetfd)
+ else:
+ self.tmpfile = EncodedFile(
+ TemporaryFile(buffering=0),
+ encoding="utf-8",
+ errors="replace",
+ newline="",
+ write_through=True,
+ )
+ if targetfd in patchsysdict:
+ self.syscapture = SysCapture(targetfd, self.tmpfile)
+ else:
+ self.syscapture = NoCapture(targetfd)
+
+ self._state = "initialized"
+
+ def __repr__(self) -> str:
+ return (
+ f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
+ f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
+ )
+
+ def _assert_state(self, op: str, states: tuple[str, ...]) -> None:
+ assert self._state in states, (
+ "cannot {} in state {!r}: expected one of {}".format(
+ op, self._state, ", ".join(states)
+ )
+ )
+
+ def start(self) -> None:
+ """Start capturing on targetfd using memorized tmpfile."""
+ self._assert_state("start", ("initialized",))
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
+ self.syscapture.start()
+ self._state = "started"
+
+ def done(self) -> None:
+ """Stop capturing, restore streams, return original capture file,
+ seeked to position zero."""
+ self._assert_state("done", ("initialized", "started", "suspended", "done"))
+ if self._state == "done":
+ return
+ os.dup2(self.targetfd_save, self.targetfd)
+ os.close(self.targetfd_save)
+ if self.targetfd_invalid is not None:
+ if self.targetfd_invalid != self.targetfd:
+ os.close(self.targetfd)
+ os.close(self.targetfd_invalid)
+ self.syscapture.done()
+ self.tmpfile.close()
+ self._state = "done"
+
+ def suspend(self) -> None:
+ self._assert_state("suspend", ("started", "suspended"))
+ if self._state == "suspended":
+ return
+ self.syscapture.suspend()
+ os.dup2(self.targetfd_save, self.targetfd)
+ self._state = "suspended"
+
+ def resume(self) -> None:
+ self._assert_state("resume", ("started", "suspended"))
+ if self._state == "started":
+ return
+ self.syscapture.resume()
+ os.dup2(self.tmpfile.fileno(), self.targetfd)
+ self._state = "started"
+
+
+class FDCaptureBinary(FDCaptureBase[bytes]):
+ """Capture IO to/from a given OS-level file descriptor.
+
+ snap() produces `bytes`.
+ """
+
+ EMPTY_BUFFER = b""
+
+ def snap(self) -> bytes:
+ self._assert_state("snap", ("started", "suspended"))
+ self.tmpfile.seek(0)
+ res = self.tmpfile.buffer.read()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res # type: ignore[return-value]
+
+ def writeorg(self, data: bytes) -> None:
+ """Write to original file descriptor."""
+ self._assert_state("writeorg", ("started", "suspended"))
+ os.write(self.targetfd_save, data)
+
+
+class FDCapture(FDCaptureBase[str]):
+ """Capture IO to/from a given OS-level file descriptor.
+
+ snap() produces text.
+ """
+
+ EMPTY_BUFFER = ""
+
+ def snap(self) -> str:
+ self._assert_state("snap", ("started", "suspended"))
+ self.tmpfile.seek(0)
+ res = self.tmpfile.read()
+ self.tmpfile.seek(0)
+ self.tmpfile.truncate()
+ return res
+
+ def writeorg(self, data: str) -> None:
+ """Write to original file descriptor."""
+ self._assert_state("writeorg", ("started", "suspended"))
+ # XXX use encoding of original stream
+ os.write(self.targetfd_save, data.encode("utf-8"))
+
+
+# MultiCapture
+
+
+# Generic NamedTuple only supported since Python 3.11.
+if sys.version_info >= (3, 11) or TYPE_CHECKING:
+
+ @final
+ class CaptureResult(NamedTuple, Generic[AnyStr]):
+ """The result of :method:`caplog.readouterr() `."""
+
+ out: AnyStr
+ err: AnyStr
+
+else:
+
+ class CaptureResult(
+ collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
+ Generic[AnyStr],
+ ):
+ """The result of :method:`caplog.readouterr() `."""
+
+ __slots__ = ()
+
+
+class MultiCapture(Generic[AnyStr]):
+ _state = None
+ _in_suspended = False
+
+ def __init__(
+ self,
+ in_: CaptureBase[AnyStr] | None,
+ out: CaptureBase[AnyStr] | None,
+ err: CaptureBase[AnyStr] | None,
+ ) -> None:
+ self.in_: CaptureBase[AnyStr] | None = in_
+ self.out: CaptureBase[AnyStr] | None = out
+ self.err: CaptureBase[AnyStr] | None = err
+
+ def __repr__(self) -> str:
+ return (
+ f""
+ )
+
+ def start_capturing(self) -> None:
+ self._state = "started"
+ if self.in_:
+ self.in_.start()
+ if self.out:
+ self.out.start()
+ if self.err:
+ self.err.start()
+
+ def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]:
+ """Pop current snapshot out/err capture and flush to orig streams."""
+ out, err = self.readouterr()
+ if out:
+ assert self.out is not None
+ self.out.writeorg(out)
+ if err:
+ assert self.err is not None
+ self.err.writeorg(err)
+ return out, err
+
+ def suspend_capturing(self, in_: bool = False) -> None:
+ self._state = "suspended"
+ if self.out:
+ self.out.suspend()
+ if self.err:
+ self.err.suspend()
+ if in_ and self.in_:
+ self.in_.suspend()
+ self._in_suspended = True
+
+ def resume_capturing(self) -> None:
+ self._state = "started"
+ if self.out:
+ self.out.resume()
+ if self.err:
+ self.err.resume()
+ if self._in_suspended:
+ assert self.in_ is not None
+ self.in_.resume()
+ self._in_suspended = False
+
+ def stop_capturing(self) -> None:
+ """Stop capturing and reset capturing streams."""
+ if self._state == "stopped":
+ raise ValueError("was already stopped")
+ self._state = "stopped"
+ if self.out:
+ self.out.done()
+ if self.err:
+ self.err.done()
+ if self.in_:
+ self.in_.done()
+
+ def is_started(self) -> bool:
+ """Whether actively capturing -- not suspended or stopped."""
+ return self._state == "started"
+
+ def readouterr(self) -> CaptureResult[AnyStr]:
+ out = self.out.snap() if self.out else ""
+ err = self.err.snap() if self.err else ""
+ # TODO: This type error is real, need to fix.
+ return CaptureResult(out, err) # type: ignore[arg-type]
+
+
+def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
+ if method == "fd":
+ return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
+ elif method == "sys":
+ return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))
+ elif method == "no":
+ return MultiCapture(in_=None, out=None, err=None)
+ elif method == "tee-sys":
+ return MultiCapture(
+ in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
+ )
+ raise ValueError(f"unknown capturing method: {method!r}")
+
+
+# CaptureManager and CaptureFixture
+
+
+class CaptureManager:
+ """The capture plugin.
+
+ Manages that the appropriate capture method is enabled/disabled during
+ collection and each test phase (setup, call, teardown). After each of
+ those points, the captured output is obtained and attached to the
+ collection/runtest report.
+
+ There are two levels of capture:
+
+ * global: enabled by default and can be suppressed by the ``-s``
+ option. This is always enabled/disabled during collection and each test
+ phase.
+
+ * fixture: when a test function or one of its fixture depend on the
+ ``capsys`` or ``capfd`` fixtures. In this case special handling is
+ needed to ensure the fixtures take precedence over the global capture.
+ """
+
+ def __init__(self, method: _CaptureMethod) -> None:
+ self._method: Final = method
+ self._global_capturing: MultiCapture[str] | None = None
+ self._capture_fixture: CaptureFixture[Any] | None = None
+
+ def __repr__(self) -> str:
+ return (
+ f""
+ )
+
+ def is_capturing(self) -> str | bool:
+ if self.is_globally_capturing():
+ return "global"
+ if self._capture_fixture:
+ return f"fixture {self._capture_fixture.request.fixturename}"
+ return False
+
+ # Global capturing control
+
+ def is_globally_capturing(self) -> bool:
+ return self._method != "no"
+
+ def start_global_capturing(self) -> None:
+ assert self._global_capturing is None
+ self._global_capturing = _get_multicapture(self._method)
+ self._global_capturing.start_capturing()
+
+ def stop_global_capturing(self) -> None:
+ if self._global_capturing is not None:
+ self._global_capturing.pop_outerr_to_orig()
+ self._global_capturing.stop_capturing()
+ self._global_capturing = None
+
+ def resume_global_capture(self) -> None:
+ # During teardown of the python process, and on rare occasions, capture
+ # attributes can be `None` while trying to resume global capture.
+ if self._global_capturing is not None:
+ self._global_capturing.resume_capturing()
+
+ def suspend_global_capture(self, in_: bool = False) -> None:
+ if self._global_capturing is not None:
+ self._global_capturing.suspend_capturing(in_=in_)
+
+ def suspend(self, in_: bool = False) -> None:
+ # Need to undo local capsys-et-al if it exists before disabling global capture.
+ self.suspend_fixture()
+ self.suspend_global_capture(in_)
+
+ def resume(self) -> None:
+ self.resume_global_capture()
+ self.resume_fixture()
+
+ def read_global_capture(self) -> CaptureResult[str]:
+ assert self._global_capturing is not None
+ return self._global_capturing.readouterr()
+
+ # Fixture Control
+
+ def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None:
+ if self._capture_fixture:
+ current_fixture = self._capture_fixture.request.fixturename
+ requested_fixture = capture_fixture.request.fixturename
+ capture_fixture.request.raiseerror(
+ f"cannot use {requested_fixture} and {current_fixture} at the same time"
+ )
+ self._capture_fixture = capture_fixture
+
+ def unset_fixture(self) -> None:
+ self._capture_fixture = None
+
+ def activate_fixture(self) -> None:
+ """If the current item is using ``capsys`` or ``capfd``, activate
+ them so they take precedence over the global capture."""
+ if self._capture_fixture:
+ self._capture_fixture._start()
+
+ def deactivate_fixture(self) -> None:
+ """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
+ if self._capture_fixture:
+ self._capture_fixture.close()
+
+ def suspend_fixture(self) -> None:
+ if self._capture_fixture:
+ self._capture_fixture._suspend()
+
+ def resume_fixture(self) -> None:
+ if self._capture_fixture:
+ self._capture_fixture._resume()
+
+ # Helper context managers
+
+ @contextlib.contextmanager
+ def global_and_fixture_disabled(self) -> Generator[None]:
+ """Context manager to temporarily disable global and current fixture capturing."""
+ do_fixture = self._capture_fixture and self._capture_fixture._is_started()
+ if do_fixture:
+ self.suspend_fixture()
+ do_global = self._global_capturing and self._global_capturing.is_started()
+ if do_global:
+ self.suspend_global_capture()
+ try:
+ yield
+ finally:
+ if do_global:
+ self.resume_global_capture()
+ if do_fixture:
+ self.resume_fixture()
+
+ @contextlib.contextmanager
+ def item_capture(self, when: str, item: Item) -> Generator[None]:
+ self.resume_global_capture()
+ self.activate_fixture()
+ try:
+ yield
+ finally:
+ self.deactivate_fixture()
+ self.suspend_global_capture(in_=False)
+
+ out, err = self.read_global_capture()
+ item.add_report_section(when, "stdout", out)
+ item.add_report_section(when, "stderr", err)
+
+ # Hooks
+
+ @hookimpl(wrapper=True)
+ def pytest_make_collect_report(
+ self, collector: Collector
+ ) -> Generator[None, CollectReport, CollectReport]:
+ if isinstance(collector, File):
+ self.resume_global_capture()
+ try:
+ rep = yield
+ finally:
+ self.suspend_global_capture()
+ out, err = self.read_global_capture()
+ if out:
+ rep.sections.append(("Captured stdout", out))
+ if err:
+ rep.sections.append(("Captured stderr", err))
+ else:
+ rep = yield
+ return rep
+
+ @hookimpl(wrapper=True)
+ def pytest_runtest_setup(self, item: Item) -> Generator[None]:
+ with self.item_capture("setup", item):
+ return (yield)
+
+ @hookimpl(wrapper=True)
+ def pytest_runtest_call(self, item: Item) -> Generator[None]:
+ with self.item_capture("call", item):
+ return (yield)
+
+ @hookimpl(wrapper=True)
+ def pytest_runtest_teardown(self, item: Item) -> Generator[None]:
+ with self.item_capture("teardown", item):
+ return (yield)
+
+ @hookimpl(tryfirst=True)
+ def pytest_keyboard_interrupt(self) -> None:
+ self.stop_global_capturing()
+
+ @hookimpl(tryfirst=True)
+ def pytest_internalerror(self) -> None:
+ self.stop_global_capturing()
+
+
+class CaptureFixture(Generic[AnyStr]):
+ """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
+ :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
+
+ def __init__(
+ self,
+ captureclass: type[CaptureBase[AnyStr]],
+ request: SubRequest,
+ *,
+ config: dict[str, Any] | None = None,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self.captureclass: type[CaptureBase[AnyStr]] = captureclass
+ self.request = request
+ self._config = config if config else {}
+ self._capture: MultiCapture[AnyStr] | None = None
+ self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
+ self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
+
+ def _start(self) -> None:
+ if self._capture is None:
+ self._capture = MultiCapture(
+ in_=None,
+ out=self.captureclass(1, **self._config),
+ err=self.captureclass(2, **self._config),
+ )
+ self._capture.start_capturing()
+
+ def close(self) -> None:
+ if self._capture is not None:
+ if self._config.get("tee"):
+ # When tee is enabled, output was already written to the
+ # original stream in real-time by TeeCaptureIO. Using
+ # pop_outerr_to_orig() would write it a second time via
+ # writeorg(), causing doubled output (see #13784).
+ out, err = self._capture.readouterr()
+ else:
+ out, err = self._capture.pop_outerr_to_orig()
+ self._captured_out += out
+ self._captured_err += err
+ self._capture.stop_capturing()
+ self._capture = None
+
+ def readouterr(self) -> CaptureResult[AnyStr]:
+ """Read and return the captured output so far, resetting the internal
+ buffer.
+
+ :returns:
+ The captured content as a namedtuple with ``out`` and ``err``
+ string attributes.
+ """
+ captured_out, captured_err = self._captured_out, self._captured_err
+ if self._capture is not None:
+ out, err = self._capture.readouterr()
+ captured_out += out
+ captured_err += err
+ self._captured_out = self.captureclass.EMPTY_BUFFER
+ self._captured_err = self.captureclass.EMPTY_BUFFER
+ return CaptureResult(captured_out, captured_err)
+
+ def _suspend(self) -> None:
+ """Suspend this fixture's own capturing temporarily."""
+ if self._capture is not None:
+ self._capture.suspend_capturing()
+
+ def _resume(self) -> None:
+ """Resume this fixture's own capturing temporarily."""
+ if self._capture is not None:
+ self._capture.resume_capturing()
+
+ def _is_started(self) -> bool:
+ """Whether actively capturing -- not disabled or closed."""
+ if self._capture is not None:
+ return self._capture.is_started()
+ return False
+
+ @contextlib.contextmanager
+ def disabled(self) -> Generator[None]:
+ """Temporarily disable capturing while inside the ``with`` block."""
+ capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
+ "capturemanager"
+ )
+ with capmanager.global_and_fixture_disabled():
+ yield
+
+
+# The fixtures.
+
+
+@fixture
+def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]:
+ r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
+
+ The captured output is made available via ``capsys.readouterr()`` method
+ calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``text`` objects.
+
+ Returns an instance of :class:`CaptureFixture[str] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_output(capsys):
+ print("hello")
+ captured = capsys.readouterr()
+ assert captured.out == "hello\n"
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]:
+ r"""Enable simultaneous text capturing and pass-through of writes
+ to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``.
+
+
+ The captured output is made available via ``capteesys.readouterr()`` method
+ calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``text`` objects.
+
+ The output is also passed-through, allowing it to be "live-printed",
+ reported, or both as defined by ``--capture=``.
+
+ Returns an instance of :class:`CaptureFixture[str] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_output(capteesys):
+ print("hello")
+ captured = capteesys.readouterr()
+ assert captured.out == "hello\n"
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(
+ SysCapture, request, config=dict(tee=True), _ispytest=True
+ )
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
+ r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
+
+ The captured output is made available via ``capsysbinary.readouterr()``
+ method calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``bytes`` objects.
+
+ Returns an instance of :class:`CaptureFixture[bytes] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_output(capsysbinary):
+ print("hello")
+ captured = capsysbinary.readouterr()
+ assert captured.out == b"hello\n"
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]:
+ r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
+
+ The captured output is made available via ``capfd.readouterr()`` method
+ calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``text`` objects.
+
+ Returns an instance of :class:`CaptureFixture[str] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_system_echo(capfd):
+ os.system('echo "hello"')
+ captured = capfd.readouterr()
+ assert captured.out == "hello\n"
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
+
+
+@fixture
+def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]:
+ r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
+
+ The captured output is made available via ``capfd.readouterr()`` method
+ calls, which return a ``(out, err)`` namedtuple.
+ ``out`` and ``err`` will be ``byte`` objects.
+
+ Returns an instance of :class:`CaptureFixture[bytes] `.
+
+ Example:
+
+ .. code-block:: python
+
+ def test_system_echo(capfdbinary):
+ os.system('echo "hello"')
+ captured = capfdbinary.readouterr()
+ assert captured.out == b"hello\n"
+
+ """
+ capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
+ capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
+ capman.set_fixture(capture_fixture)
+ capture_fixture._start()
+ yield capture_fixture
+ capture_fixture.close()
+ capman.unset_fixture()
diff --git a/venv/lib/python3.11/site-packages/_pytest/compat.py b/venv/lib/python3.11/site-packages/_pytest/compat.py
new file mode 100644
index 000000000..d3b2a4696
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/compat.py
@@ -0,0 +1,329 @@
+# mypy: allow-untyped-defs
+"""Python version compatibility code and random general utilities."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+import enum
+import functools
+import inspect
+from inspect import Parameter
+from inspect import Signature
+import os
+from pathlib import Path
+import sys
+from typing import Any
+from typing import Final
+from typing import NoReturn
+from typing import TYPE_CHECKING
+
+import py
+
+
+if sys.version_info >= (3, 14):
+ from annotationlib import Format
+
+
+#: constant to prepare valuing pylib path replacements/lazy proxies later on
+# intended for removal in pytest 8.0 or 9.0
+
+# fmt: off
+# intentional space to create a fake difference for the verification
+LEGACY_PATH = py.path. local
+# fmt: on
+
+
+def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
+ """Internal wrapper to prepare lazy proxies for legacy_path instances"""
+ return LEGACY_PATH(path)
+
+
+# fmt: off
+# Singleton type for NOTSET, as described in:
+# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
+class NotSetType(enum.Enum):
+ token = 0
+NOTSET: Final = NotSetType.token
+# fmt: on
+
+
+def iscoroutinefunction(func: object) -> bool:
+ """Return True if func is a coroutine function (a function defined with async
+ def syntax, and doesn't contain yield), or a function decorated with
+ @asyncio.coroutine.
+
+ Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
+ importing asyncio directly, which in turns also initializes the "logging"
+ module as a side-effect (see issue #8).
+ """
+ return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
+
+
+def is_async_function(func: object) -> bool:
+ """Return True if the given function seems to be an async function or
+ an async generator."""
+ return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
+
+
+def signature(obj: Callable[..., Any]) -> Signature:
+ """Return signature without evaluating annotations."""
+ if sys.version_info >= (3, 14):
+ return inspect.signature(obj, annotation_format=Format.STRING)
+ return inspect.signature(obj)
+
+
+def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
+ function = get_real_func(function)
+ fn = Path(inspect.getfile(function))
+ lineno = function.__code__.co_firstlineno
+ if curdir is not None:
+ try:
+ relfn = fn.relative_to(curdir)
+ except ValueError:
+ pass
+ else:
+ return f"{relfn}:{lineno + 1}"
+ return f"{fn}:{lineno + 1}"
+
+
+def num_mock_patch_args(function) -> int:
+ """Return number of arguments used up by mock arguments (if any)."""
+ patchings = getattr(function, "patchings", None)
+ if not patchings:
+ return 0
+
+ mock_sentinel = getattr(sys.modules.get("mock"), "DEFAULT", object())
+ ut_mock_sentinel = getattr(sys.modules.get("unittest.mock"), "DEFAULT", object())
+
+ return len(
+ [
+ p
+ for p in patchings
+ if not p.attribute_name
+ and (p.new is mock_sentinel or p.new is ut_mock_sentinel)
+ ]
+ )
+
+
+def getfuncargnames(
+ function: Callable[..., object],
+ *,
+ name: str = "",
+ cls: type | None = None,
+) -> tuple[str, ...]:
+ """Return the names of a function's mandatory arguments.
+
+ Should return the names of all function arguments that:
+ * Aren't bound to an instance or type as in instance or class methods.
+ * Don't have default values.
+ * Aren't bound with functools.partial.
+ * Aren't replaced with mocks.
+
+ The cls arguments indicate that the function should be treated as a bound
+ method even though it's not unless the function is a static method.
+
+ The name parameter should be the original name in which the function was collected.
+ """
+ # TODO(RonnyPfannschmidt): This function should be refactored when we
+ # revisit fixtures. The fixture mechanism should ask the node for
+ # the fixture names, and not try to obtain directly from the
+ # function object well after collection has occurred.
+
+ # The parameters attribute of a Signature object contains an
+ # ordered mapping of parameter names to Parameter instances. This
+ # creates a tuple of the names of the parameters that don't have
+ # defaults.
+ try:
+ parameters = signature(function).parameters.values()
+ except (ValueError, TypeError) as e:
+ from _pytest.outcomes import fail
+
+ fail(
+ f"Could not determine arguments of {function!r}: {e}",
+ pytrace=False,
+ )
+
+ arg_names = tuple(
+ p.name
+ for p in parameters
+ if (
+ p.kind is Parameter.POSITIONAL_OR_KEYWORD
+ or p.kind is Parameter.KEYWORD_ONLY
+ )
+ and p.default is Parameter.empty
+ )
+ if not name:
+ name = function.__name__
+
+ # If this function should be treated as a bound method even though
+ # it's passed as an unbound method or function, and its first parameter
+ # wasn't defined as positional only, remove the first parameter name.
+ if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and (
+ # Not using `getattr` because we don't want to resolve the staticmethod.
+ # Not using `cls.__dict__` because we want to check the entire MRO.
+ cls
+ and not isinstance(
+ inspect.getattr_static(cls, name, default=None), staticmethod
+ )
+ ):
+ arg_names = arg_names[1:]
+ # Remove any names that will be replaced with mocks.
+ if hasattr(function, "__wrapped__"):
+ arg_names = arg_names[num_mock_patch_args(function) :]
+ return arg_names
+
+
+def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
+ # Note: this code intentionally mirrors the code at the beginning of
+ # getfuncargnames, to get the arguments which were excluded from its result
+ # because they had default values.
+ return tuple(
+ p.name
+ for p in signature(function).parameters.values()
+ if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
+ and p.default is not Parameter.empty
+ )
+
+
+_non_printable_ascii_translate_table = {
+ i: f"\\x{i:02x}" for i in range(128) if i not in range(32, 127)
+}
+_non_printable_ascii_translate_table.update(
+ {ord("\t"): "\\t", ord("\r"): "\\r", ord("\n"): "\\n"}
+)
+
+
+def ascii_escaped(val: bytes | str) -> str:
+ r"""If val is pure ASCII, return it as an str, otherwise, escape
+ bytes objects into a sequence of escaped bytes:
+
+ b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
+
+ and escapes strings into a sequence of escaped unicode ids, e.g.:
+
+ r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
+
+ Note:
+ The obvious "v.decode('unicode-escape')" will return
+ valid UTF-8 unicode if it finds them in bytes, but we
+ want to return escaped bytes for any byte, even if they match
+ a UTF-8 string.
+ """
+ if isinstance(val, bytes):
+ ret = val.decode("ascii", "backslashreplace")
+ else:
+ ret = val.encode("unicode_escape").decode("ascii")
+ return ret.translate(_non_printable_ascii_translate_table)
+
+
+def get_real_func(obj):
+ """Get the real function object of the (possibly) wrapped object by
+ :func:`functools.wraps`, or :func:`functools.partial`."""
+ obj = inspect.unwrap(obj)
+
+ if isinstance(obj, functools.partial):
+ obj = obj.func
+ return obj
+
+
+def getimfunc(func):
+ try:
+ return func.__func__
+ except AttributeError:
+ return func
+
+
+def safe_getattr(object: Any, name: str, default: Any) -> Any:
+ """Like getattr but return default upon any Exception or any OutcomeException.
+
+ Attribute access can potentially fail for 'evil' Python objects.
+ See issue #214.
+ It catches OutcomeException because of #2490 (issue #580), new outcomes
+ are derived from BaseException instead of Exception (for more details
+ check #2707).
+ """
+ from _pytest.outcomes import TEST_OUTCOME
+
+ try:
+ return getattr(object, name, default)
+ except TEST_OUTCOME:
+ return default
+
+
+def safe_isclass(obj: object) -> bool:
+ """Ignore any exception via isinstance on Python 3."""
+ try:
+ return inspect.isclass(obj)
+ except Exception:
+ return False
+
+
+def get_user_id() -> int | None:
+ """Return the current process's real user id or None if it could not be
+ determined.
+
+ :return: The user id or None if it could not be determined.
+ """
+ # mypy follows the version and platform checking expectation of PEP 484:
+ # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
+ # Containment checks are too complex for mypy v1.5.0 and cause failure.
+ if sys.platform == "win32" or sys.platform == "emscripten":
+ # win32 does not have a getuid() function.
+ # Emscripten has a return 0 stub.
+ return None
+ else:
+ # On other platforms, a return value of -1 is assumed to indicate that
+ # the current process's real user id could not be determined.
+ ERROR = -1
+ uid = os.getuid()
+ return uid if uid != ERROR else None
+
+
+if sys.version_info >= (3, 11):
+ from typing import assert_never
+else:
+
+ def assert_never(value: NoReturn) -> NoReturn:
+ assert False, f"Unhandled value: {value} ({type(value).__name__})"
+
+
+class CallableBool:
+ """
+ A bool-like object that can also be called, returning its true/false value.
+
+ Used for backwards compatibility in cases where something was supposed to be a method
+ but was implemented as a simple attribute by mistake (see `TerminalReporter.isatty`).
+
+ Do not use in new code.
+ """
+
+ def __init__(self, value: bool) -> None:
+ self._value = value
+
+ def __bool__(self) -> bool:
+ return self._value
+
+ def __call__(self) -> bool:
+ return self._value
+
+
+def running_on_ci() -> bool:
+ """Check if we're currently running on a CI system."""
+ # Only enable CI mode if one of these env variables is defined and non-empty.
+ # Note: review `regendoc` tox env in case this list is changed.
+ env_vars = ["CI", "BUILD_NUMBER"]
+ return any(os.environ.get(var) for var in env_vars)
+
+
+if sys.version_info >= (3, 13):
+ from warnings import deprecated as deprecated
+else:
+ if TYPE_CHECKING:
+ from typing_extensions import deprecated as deprecated
+ else:
+
+ def deprecated(msg, /, *, category=None, stacklevel=1):
+ def decorator(func):
+ return func
+
+ return decorator
diff --git a/venv/lib/python3.11/site-packages/_pytest/config/__init__.py b/venv/lib/python3.11/site-packages/_pytest/config/__init__.py
new file mode 100644
index 000000000..96b3dd6a8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/config/__init__.py
@@ -0,0 +1,2246 @@
+# mypy: allow-untyped-defs
+"""Command line options, config-file and conftest.py processing."""
+
+from __future__ import annotations
+
+import argparse
+import builtins
+import collections.abc
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Mapping
+from collections.abc import MutableMapping
+from collections.abc import Sequence
+import contextlib
+import copy
+import dataclasses
+import enum
+from functools import lru_cache
+import glob
+import importlib
+import importlib.metadata
+import inspect
+import os
+import pathlib
+import re
+import shlex
+import sys
+from textwrap import dedent
+import types
+from types import FunctionType
+from typing import Any
+from typing import cast
+from typing import Final
+from typing import final
+from typing import IO
+from typing import TextIO
+from typing import TYPE_CHECKING
+import warnings
+
+from pluggy import HookimplMarker
+from pluggy import HookimplOpts
+from pluggy import HookspecMarker
+from pluggy import HookspecOpts
+from pluggy import PluginManager
+
+from .exceptions import PrintHelp as PrintHelp
+from .exceptions import UsageError as UsageError
+from .findpaths import ConfigDict
+from .findpaths import ConfigValue
+from .findpaths import determine_setup
+from .findpaths import parse_override_ini
+from _pytest import __version__
+import _pytest._code
+from _pytest._code import ExceptionInfo
+from _pytest._code import filter_traceback
+from _pytest._code.code import TracebackStyle
+from _pytest._io import TerminalWriter
+from _pytest.compat import assert_never
+from _pytest.compat import deprecated
+from _pytest.compat import NOTSET
+from _pytest.config.argparsing import Argument
+from _pytest.config.argparsing import FILE_OR_DIR
+from _pytest.config.argparsing import Parser
+import _pytest.deprecated
+import _pytest.hookspec
+from _pytest.outcomes import fail
+from _pytest.outcomes import Skipped
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import import_path
+from _pytest.pathlib import ImportMode
+from _pytest.pathlib import resolve_package_path
+from _pytest.pathlib import safe_exists
+from _pytest.stash import Stash
+from _pytest.warning_types import PytestConfigWarning
+from _pytest.warning_types import warn_explicit_for
+
+
+if TYPE_CHECKING:
+ from _pytest.assertion.rewrite import AssertionRewritingHook
+ from _pytest.cacheprovider import Cache
+ from _pytest.terminal import TerminalReporter
+
+_PluggyPlugin = object
+"""A type to represent plugin objects.
+
+Plugins can be any namespace, so we can't narrow it down much, but we use an
+alias to make the intent clear.
+
+Ideally this type would be provided by pluggy itself.
+"""
+
+
+hookimpl = HookimplMarker("pytest")
+hookspec = HookspecMarker("pytest")
+
+
+@final
+class ExitCode(enum.IntEnum):
+ """Encodes the valid exit codes by pytest.
+
+ Currently users and plugins may supply other exit codes as well.
+
+ .. versionadded:: 5.0
+ """
+
+ #: Tests passed.
+ OK = 0
+ #: Tests failed.
+ TESTS_FAILED = 1
+ #: pytest was interrupted.
+ INTERRUPTED = 2
+ #: An internal error got in the way.
+ INTERNAL_ERROR = 3
+ #: pytest was misused.
+ USAGE_ERROR = 4
+ #: pytest couldn't find tests.
+ NO_TESTS_COLLECTED = 5
+ #: All tests pass, but maximum number of warnings exceeded.
+ MAX_WARNINGS_ERROR = 6
+
+ __module__ = "pytest"
+
+
+class ConftestImportFailure(Exception):
+ def __init__(
+ self,
+ path: pathlib.Path,
+ *,
+ cause: Exception,
+ ) -> None:
+ self.path = path
+ self.cause = cause
+
+ def __str__(self) -> str:
+ return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
+
+
+def filter_traceback_for_conftest_import_failure(
+ entry: _pytest._code.TracebackEntry,
+) -> bool:
+ """Filter tracebacks entries which point to pytest internals or importlib.
+
+ Make a special case for importlib because we use it to import test modules and conftest files
+ in _pytest.pathlib.import_path.
+ """
+ return filter_traceback(entry) and "importlib" not in str(entry.path).split(os.sep)
+
+
+def print_conftest_import_error(e: ConftestImportFailure, file: TextIO) -> None:
+ exc_info = ExceptionInfo.from_exception(e.cause)
+ tw = TerminalWriter(file)
+ tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
+ exc_info.traceback = exc_info.traceback.filter(
+ filter_traceback_for_conftest_import_failure
+ )
+ exc_repr = (
+ exc_info.getrepr(style="short", chain=False)
+ if exc_info.traceback
+ else exc_info.exconly()
+ )
+ formatted_tb = str(exc_repr)
+ for line in formatted_tb.splitlines():
+ tw.line(line.rstrip(), red=True)
+
+
+def print_usage_error(e: UsageError, file: TextIO) -> None:
+ tw = TerminalWriter(file)
+ for msg in e.args:
+ tw.line(f"ERROR: {msg}\n", red=True)
+
+
+def _get_prog_name(argv: Sequence[str]) -> str:
+ """Determine the CLI program name from the argument vector.
+
+ :param argv: The argument vector (typically ``sys.argv``).
+ :returns: ``"python -m pytest"`` when invoked via ``python -m``,
+ ``"pytest"`` otherwise.
+ """
+ argv0 = argv[0] if argv else ""
+ if os.path.basename(argv0) == "__main__.py":
+ return "python -m pytest"
+ return "pytest"
+
+
+def main(
+ args: list[str] | os.PathLike[str] | None = None,
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
+) -> int | ExitCode:
+ """Perform an in-process test run.
+
+ :param args:
+ List of command line arguments. If `None` or not given, defaults to reading
+ arguments directly from the process command line (:data:`sys.argv`).
+ :param plugins: List of plugin objects to be auto-registered during initialization.
+
+ :returns: An exit code.
+ """
+ return _main(args=args, plugins=plugins, prog="pytest.main()")
+
+
+def _main(
+ *,
+ args: list[str] | os.PathLike[str] | None = None,
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
+ prog: str,
+) -> int | ExitCode:
+ # Handle a single `--version`/`-V` argument early to avoid starting up the entire pytest infrastructure.
+ new_args = sys.argv[1:] if args is None else args
+ if (
+ isinstance(new_args, Sequence)
+ and (new_args.count("--version") + new_args.count("-V")) == 1
+ ):
+ sys.stdout.write(f"pytest {__version__}\n")
+ return ExitCode.OK
+
+ old_pytest_version = os.environ.get("PYTEST_VERSION")
+ try:
+ os.environ["PYTEST_VERSION"] = __version__
+ try:
+ config = _prepareconfig(new_args, plugins, prog=prog)
+ except ConftestImportFailure as e:
+ print_conftest_import_error(e, file=sys.stderr)
+ return ExitCode.USAGE_ERROR
+
+ try:
+ ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
+ try:
+ return ExitCode(ret)
+ except ValueError:
+ return ret
+ finally:
+ config._ensure_unconfigure()
+ except UsageError as e:
+ print_usage_error(e, file=sys.stderr)
+ return ExitCode.USAGE_ERROR
+ finally:
+ if old_pytest_version is None:
+ os.environ.pop("PYTEST_VERSION", None)
+ else:
+ os.environ["PYTEST_VERSION"] = old_pytest_version
+
+
+def _console_main() -> int:
+ """The CLI entry point of pytest (internal).
+
+ This is the real implementation used by entry points and ``__main__.py``.
+ """
+ # https://docs.python.org/3/library/signal.html#note-on-sigpipe
+ try:
+ code = _main(prog=_get_prog_name(sys.argv))
+ sys.stdout.flush()
+ return code
+ except BrokenPipeError:
+ # Python flushes standard streams on exit; redirect remaining output
+ # to devnull to avoid another BrokenPipeError at shutdown
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ os.dup2(devnull, sys.stdout.fileno())
+ return 1 # Python exits with error code 1 on EPIPE
+
+
+def console_main() -> int:
+ """The CLI entry point of pytest.
+
+ .. deprecated:: 9.1
+ This function is slated for removal in pytest 10.
+ It is not meant for programmable use; use :func:`pytest.main` instead.
+ """
+ import warnings
+
+ from _pytest.deprecated import CONSOLE_MAIN
+
+ warnings.warn(CONSOLE_MAIN, stacklevel=2)
+ return _console_main()
+
+
+class cmdline: # compatibility namespace
+ main = staticmethod(main)
+
+
+def filename_arg(path: str, optname: str) -> str:
+ """Argparse type validator for filename arguments.
+
+ :path: Path of filename.
+ :optname: Name of the option.
+ """
+ if os.path.isdir(path):
+ raise UsageError(f"{optname} must be a filename, given: {path}")
+ return path
+
+
+def directory_arg(path: str, optname: str) -> str:
+ """Argparse type validator for directory arguments.
+
+ :path: Path of directory.
+ :optname: Name of the option.
+ """
+ if not os.path.isdir(path):
+ raise UsageError(f"{optname} must be a directory, given: {path}")
+ return path
+
+
+# Plugins that cannot be disabled via "-p no:X" currently.
+essential_plugins = (
+ "mark",
+ "main",
+ "runner",
+ "fixtures",
+ "helpconfig", # Provides -p.
+)
+
+default_plugins = (
+ *essential_plugins,
+ "python",
+ "terminal",
+ "debugging",
+ "unittest",
+ "capture",
+ "skipping",
+ "legacypath",
+ "tmpdir",
+ "monkeypatch",
+ "recwarn",
+ "pastebin",
+ "assertion",
+ "junitxml",
+ "doctest",
+ "cacheprovider",
+ "setuponly",
+ "setupplan",
+ "stepwise",
+ "unraisableexception",
+ "threadexception",
+ "warnings",
+ "logging",
+ "reports",
+ "faulthandler",
+ "subtests",
+)
+
+builtin_plugins = {
+ *default_plugins,
+ "pytester",
+ "pytester_assertions",
+ "terminalprogress",
+}
+
+
+def get_config(
+ args: Iterable[str] | None = None,
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
+ *,
+ prog: str | None = None,
+) -> Config:
+ # Subsequent calls to main will create a fresh instance.
+ pluginmanager = PytestPluginManager()
+ invocation_params = Config.InvocationParams(
+ args=args or (),
+ plugins=plugins,
+ dir=pathlib.Path.cwd(),
+ )
+ config = Config(pluginmanager, invocation_params=invocation_params, prog=prog)
+
+ if invocation_params.args:
+ # Handle any "-p no:plugin" args.
+ pluginmanager.consider_preparse(invocation_params.args, exclude_only=True)
+
+ for spec in default_plugins:
+ pluginmanager.import_plugin(spec)
+
+ return config
+
+
+def get_plugin_manager() -> PytestPluginManager:
+ """Obtain a new instance of the
+ :py:class:`pytest.PytestPluginManager`, with default plugins
+ already loaded.
+
+ This function can be used by integration with other tools, like hooking
+ into pytest to run tests into an IDE.
+ """
+ return get_config().pluginmanager
+
+
+def _prepareconfig(
+ args: list[str] | os.PathLike[str],
+ plugins: Sequence[str | _PluggyPlugin] | None = None,
+ *,
+ prog: str | None = None,
+) -> Config:
+ if isinstance(args, os.PathLike):
+ args = [os.fspath(args)]
+ elif not isinstance(args, list):
+ msg = ( # type:ignore[unreachable]
+ "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
+ )
+ raise TypeError(msg.format(args, type(args)))
+
+ initial_config = get_config(args, plugins, prog=prog)
+ pluginmanager = initial_config.pluginmanager
+ try:
+ if plugins:
+ for plugin in plugins:
+ if isinstance(plugin, str):
+ pluginmanager.consider_pluginarg(plugin)
+ else:
+ pluginmanager.register(plugin)
+ config: Config = pluginmanager.hook.pytest_cmdline_parse(
+ pluginmanager=pluginmanager, args=args
+ )
+ return config
+ except BaseException:
+ initial_config._ensure_unconfigure()
+ raise
+
+
+def _get_directory(path: pathlib.Path) -> pathlib.Path:
+ """Get the directory of a path - itself if already a directory."""
+ if path.is_file():
+ return path.parent
+ else:
+ return path
+
+
+def _get_legacy_hook_marks(
+ method: Any,
+ hook_type: str,
+ opt_names: tuple[str, ...],
+) -> dict[str, bool]:
+ if TYPE_CHECKING:
+ # abuse typeguard from importlib to avoid massive method type union that's lacking an alias
+ assert inspect.isroutine(method)
+ known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
+ must_warn: list[str] = []
+ opts: dict[str, bool] = {}
+ for opt_name in opt_names:
+ opt_attr = getattr(method, opt_name, AttributeError)
+ if opt_attr is not AttributeError:
+ must_warn.append(f"{opt_name}={opt_attr}")
+ opts[opt_name] = True
+ elif opt_name in known_marks:
+ must_warn.append(f"{opt_name}=True")
+ opts[opt_name] = True
+ else:
+ opts[opt_name] = False
+ if must_warn:
+ hook_opts = ", ".join(must_warn)
+ message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
+ type=hook_type,
+ fullname=method.__qualname__,
+ hook_opts=hook_opts,
+ )
+ warn_explicit_for(cast(FunctionType, method), message)
+ return opts
+
+
+@final
+class PytestPluginManager(PluginManager):
+ """A :py:class:`pluggy.PluginManager ` with
+ additional pytest-specific functionality:
+
+ * Loading plugins from the command line, ``PYTEST_PLUGINS`` env variable and
+ ``pytest_plugins`` global variables found in plugins being loaded.
+ * ``conftest.py`` loading during start-up.
+ """
+
+ def __init__(self) -> None:
+ from _pytest.assertion import DummyRewriteHook
+ from _pytest.assertion import RewriteHook
+
+ super().__init__("pytest")
+
+ # -- State related to local conftest plugins.
+ # All loaded conftest modules.
+ self._conftest_plugins: set[types.ModuleType] = set()
+ # All conftest modules applicable for a directory.
+ # This includes the directory's own conftest modules as well
+ # as those of its parent directories.
+ self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {}
+ # Cutoff directory above which conftests are no longer discovered.
+ self._confcutdir: pathlib.Path | None = None
+ # If set, conftest loading is skipped.
+ self._noconftest = False
+
+ # _getconftestmodules()'s call to _get_directory() causes a stat
+ # storm when it's called potentially thousands of times in a test
+ # session (#9478), often with the same path, so cache it.
+ self._get_directory = lru_cache(256)(_get_directory)
+
+ # plugins that were explicitly skipped with pytest.skip
+ # list of (module name, skip reason)
+ # previously we would issue a warning when a plugin was skipped, but
+ # since we refactored warnings as first citizens of Config, they are
+ # just stored here to be used later.
+ self.skipped_plugins: list[tuple[str, str]] = []
+
+ self.add_hookspecs(_pytest.hookspec)
+ self.register(self)
+ if os.environ.get("PYTEST_DEBUG"):
+ err: IO[str] = sys.stderr
+ encoding: str = getattr(err, "encoding", "utf8")
+ try:
+ err = open(
+ os.dup(err.fileno()),
+ mode=err.mode,
+ buffering=1,
+ encoding=encoding,
+ )
+ except Exception:
+ pass
+ self.trace.root.setwriter(err.write)
+ self.enable_tracing()
+
+ # Config._consider_importhook will set a real object if required.
+ self.rewrite_hook: RewriteHook = DummyRewriteHook()
+ # Used to know when we are importing conftests after the pytest_configure stage.
+ self._configured = False
+
+ def parse_hookimpl_opts(
+ self, plugin: _PluggyPlugin, name: str
+ ) -> HookimplOpts | None:
+ """:meta private:"""
+ # pytest hooks are always prefixed with "pytest_",
+ # so we avoid accessing possibly non-readable attributes
+ # (see issue #1073).
+ if not name.startswith("pytest_"):
+ return None
+ # Ignore names which cannot be hooks.
+ if name == "pytest_plugins":
+ return None
+
+ opts = super().parse_hookimpl_opts(plugin, name)
+ if opts is not None:
+ return opts
+
+ method = getattr(plugin, name)
+ # Consider only actual functions for hooks (#3775).
+ if not inspect.isroutine(method):
+ return None
+ # Collect unmarked hooks as long as they have the `pytest_' prefix.
+ legacy = _get_legacy_hook_marks(
+ method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
+ )
+ return cast(HookimplOpts, legacy)
+
+ def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None:
+ """:meta private:"""
+ opts = super().parse_hookspec_opts(module_or_class, name)
+ if opts is None:
+ method = getattr(module_or_class, name)
+ if name.startswith("pytest_"):
+ legacy = _get_legacy_hook_marks(
+ method, "spec", ("firstresult", "historic")
+ )
+ opts = cast(HookspecOpts, legacy)
+ return opts
+
+ def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None:
+ if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS:
+ warnings.warn(
+ PytestConfigWarning(
+ "{} plugin has been merged into the core, "
+ "please remove it from your requirements.".format(
+ name.replace("_", "-")
+ )
+ )
+ )
+ return None
+ plugin_name = super().register(plugin, name)
+ if plugin_name is not None:
+ self.hook.pytest_plugin_registered.call_historic(
+ kwargs=dict(
+ plugin=plugin,
+ plugin_name=plugin_name,
+ manager=self,
+ )
+ )
+
+ if isinstance(plugin, types.ModuleType):
+ self.consider_module(plugin)
+ return plugin_name
+
+ def getplugin(self, name: str):
+ # Support deprecated naming because plugins (xdist e.g.) use it.
+ plugin: _PluggyPlugin | None = self.get_plugin(name)
+ return plugin
+
+ def hasplugin(self, name: str) -> bool:
+ """Return whether a plugin with the given name is registered."""
+ return bool(self.get_plugin(name))
+
+ def pytest_configure(self, config: Config) -> None:
+ """:meta private:"""
+ # XXX now that the pluginmanager exposes hookimpl(tryfirst...)
+ # we should remove tryfirst/trylast as markers.
+ config.addinivalue_line(
+ "markers",
+ "tryfirst: mark a hook implementation function such that the "
+ "plugin machinery will try to call it first/as early as possible. "
+ "DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "trylast: mark a hook implementation function such that the "
+ "plugin machinery will try to call it last/as late as possible. "
+ "DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",
+ )
+ self._configured = True
+
+ #
+ # Internal API for local conftest plugin handling.
+ #
+ def _set_initial_conftests(
+ self,
+ args: Sequence[str | pathlib.Path],
+ pyargs: bool,
+ noconftest: bool,
+ rootpath: pathlib.Path,
+ confcutdir: pathlib.Path | None,
+ invocation_dir: pathlib.Path,
+ importmode: ImportMode | str,
+ *,
+ consider_namespace_packages: bool,
+ ) -> None:
+ """Load initial conftest files given a preparsed "namespace".
+
+ As conftest files may add their own command line options which have
+ arguments ('--my-opt somepath') we might get some false positives.
+ All builtin and 3rd party plugins will have been loaded, however, so
+ common options will not confuse our logic here.
+ """
+ self._confcutdir = (
+ absolutepath(invocation_dir / confcutdir) if confcutdir else None
+ )
+ self._noconftest = noconftest
+ self._using_pyargs = pyargs
+
+ anchors = []
+ for initial_path in args:
+ path = str(initial_path)
+ # remove node-id syntax
+ i = path.find("::")
+ if i != -1:
+ path = path[:i]
+ anchor = absolutepath(invocation_dir / path)
+ # Ensure we do not break if what appears to be an anchor
+ # is in fact a very long option (#10169, #11394).
+ if not safe_exists(anchor):
+ continue
+
+ anchors.append(anchor)
+ # Let's also consider test* subdirs.
+ if anchor.is_dir():
+ anchors.extend(x for x in anchor.glob("test*") if x.is_dir())
+ if not anchors:
+ anchors.append(invocation_dir)
+ anchors.extend(x for x in invocation_dir.glob("test*") if x.is_dir())
+
+ for anchor in anchors:
+ self._loadconftestmodules(
+ anchor,
+ importmode,
+ rootpath,
+ consider_namespace_packages=consider_namespace_packages,
+ )
+
+ def _is_in_confcutdir(self, path: pathlib.Path) -> bool:
+ """Whether to consider the given path to load conftests from."""
+ if self._confcutdir is None:
+ return True
+ # The semantics here are literally:
+ # Do not load a conftest if it is found upwards from confcut dir.
+ # But this is *not* the same as:
+ # Load only conftests from confcutdir or below.
+ # At first glance they might seem the same thing, however we do support use cases where
+ # we want to load conftests that are not found in confcutdir or below, but are found
+ # in completely different directory hierarchies like packages installed
+ # in out-of-source trees.
+ # (see #9767 for a regression where the logic was inverted).
+ return path not in self._confcutdir.parents
+
+ def _loadconftestmodules(
+ self,
+ path: pathlib.Path,
+ importmode: str | ImportMode,
+ rootpath: pathlib.Path,
+ *,
+ consider_namespace_packages: bool,
+ ) -> None:
+ if self._noconftest:
+ return
+
+ directory = self._get_directory(path)
+
+ # Optimization: avoid repeated searches in the same directory.
+ # Assumes always called with same importmode and rootpath.
+ if directory in self._dirpath2confmods:
+ return
+
+ clist = []
+ for parent in reversed((directory, *directory.parents)):
+ if self._is_in_confcutdir(parent):
+ conftestpath = parent / "conftest.py"
+ if conftestpath.is_file():
+ mod = self._importconftest(
+ conftestpath,
+ importmode,
+ rootpath,
+ consider_namespace_packages=consider_namespace_packages,
+ )
+ clist.append(mod)
+ self._dirpath2confmods[directory] = clist
+
+ def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]:
+ directory = self._get_directory(path)
+ return self._dirpath2confmods.get(directory, ())
+
+ def _rget_with_confmod(
+ self,
+ name: str,
+ path: pathlib.Path,
+ ) -> tuple[types.ModuleType, Any]:
+ modules = self._getconftestmodules(path)
+ for mod in reversed(modules):
+ try:
+ return mod, getattr(mod, name)
+ except AttributeError:
+ continue
+ raise KeyError(name)
+
+ def _importconftest(
+ self,
+ conftestpath: pathlib.Path,
+ importmode: str | ImportMode,
+ rootpath: pathlib.Path,
+ *,
+ consider_namespace_packages: bool,
+ ) -> types.ModuleType:
+ conftestpath_plugin_name = str(conftestpath)
+ existing = self.get_plugin(conftestpath_plugin_name)
+ if existing is not None:
+ return cast(types.ModuleType, existing)
+
+ # conftest.py files there are not in a Python package all have module
+ # name "conftest", and thus conflict with each other. Clear the existing
+ # before loading the new one, otherwise the existing one will be
+ # returned from the module cache.
+ pkgpath = resolve_package_path(conftestpath)
+ if pkgpath is None:
+ try:
+ del sys.modules[conftestpath.stem]
+ except KeyError:
+ pass
+
+ try:
+ mod = import_path(
+ conftestpath,
+ mode=importmode,
+ root=rootpath,
+ consider_namespace_packages=consider_namespace_packages,
+ )
+ except Exception as e:
+ assert e.__traceback__ is not None
+ raise ConftestImportFailure(conftestpath, cause=e) from e
+
+ self._check_non_top_pytest_plugins(mod, conftestpath)
+
+ self._conftest_plugins.add(mod)
+ dirpath = conftestpath.parent
+ if dirpath in self._dirpath2confmods:
+ for path, mods in self._dirpath2confmods.items():
+ if dirpath in path.parents or path == dirpath:
+ if mod in mods:
+ raise AssertionError(
+ f"While trying to load conftest path {conftestpath!s}, "
+ f"found that the module {mod} is already loaded with path {mod.__file__}. "
+ "This is not supposed to happen. Please report this issue to pytest."
+ )
+ mods.append(mod)
+ self.trace(f"loading conftestmodule {mod!r}")
+ self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
+ return mod
+
+ def _check_non_top_pytest_plugins(
+ self,
+ mod: types.ModuleType,
+ conftestpath: pathlib.Path,
+ ) -> None:
+ if (
+ hasattr(mod, "pytest_plugins")
+ and self._configured
+ and not self._using_pyargs
+ ):
+ msg = (
+ "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n"
+ "It affects the entire test suite instead of just below the conftest as expected.\n"
+ " {}\n"
+ "Please move it to a top level conftest file at the rootdir:\n"
+ " {}\n"
+ "For more information, visit:\n"
+ " https://docs.pytest.org/en/stable/deprecations.html#pytest-plugins-in-non-top-level-conftest-files"
+ )
+ fail(msg.format(conftestpath, self._confcutdir), pytrace=False)
+
+ #
+ # API for bootstrapping plugin loading
+ #
+ #
+
+ def consider_preparse(
+ self, args: Sequence[str], *, exclude_only: bool = False
+ ) -> None:
+ """:meta private:"""
+ i = 0
+ n = len(args)
+ while i < n:
+ opt = args[i]
+ i += 1
+ if isinstance(opt, str):
+ if opt == "-p":
+ try:
+ parg = args[i]
+ except IndexError:
+ return
+ i += 1
+ elif opt.startswith("-p"):
+ parg = opt[2:]
+ else:
+ continue
+ parg = parg.strip()
+ if exclude_only and not parg.startswith("no:"):
+ continue
+ self.consider_pluginarg(parg)
+
+ def consider_pluginarg(self, arg: str) -> None:
+ """:meta private:"""
+ if arg.startswith("no:"):
+ name = arg[3:]
+ if name in essential_plugins:
+ raise UsageError(f"plugin {name} cannot be disabled")
+
+ if name.endswith("conftest.py"):
+ raise UsageError(
+ f"Blocking conftest files using -p is not supported: -p no:{name}\n"
+ "conftest.py files are not plugins and cannot be disabled via -p.\n"
+ )
+
+ # PR #4304: remove stepwise if cacheprovider is blocked.
+ if name == "cacheprovider":
+ self.set_blocked("stepwise")
+ self.set_blocked("pytest_stepwise")
+
+ self.set_blocked(name)
+ if not name.startswith("pytest_"):
+ self.set_blocked("pytest_" + name)
+ else:
+ name = arg
+ # Unblock the plugin.
+ self.unblock(name)
+ if not name.startswith("pytest_"):
+ self.unblock("pytest_" + name)
+ self.import_plugin(arg, consider_entry_points=True)
+
+ def consider_conftest(
+ self, conftestmodule: types.ModuleType, registration_name: str
+ ) -> None:
+ """:meta private:"""
+ self.register(conftestmodule, name=registration_name)
+
+ def consider_env(self) -> None:
+ """:meta private:"""
+ self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
+
+ def consider_module(self, mod: types.ModuleType) -> None:
+ """:meta private:"""
+ self._import_plugin_specs(getattr(mod, "pytest_plugins", []))
+
+ def _import_plugin_specs(
+ self, spec: None | types.ModuleType | str | Sequence[str]
+ ) -> None:
+ plugins = _get_plugin_specs_as_list(spec)
+ for import_spec in plugins:
+ self.import_plugin(import_spec)
+
+ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> None:
+ """Import a plugin with ``modname``.
+
+ If ``consider_entry_points`` is True, entry point names are also
+ considered to find a plugin.
+ """
+ # Most often modname refers to builtin modules, e.g. "pytester",
+ # "terminal" or "capture". Those plugins are registered under their
+ # basename for historic purposes but must be imported with the
+ # _pytest prefix.
+ assert isinstance(modname, str), (
+ f"module name as text required, got {modname!r}"
+ )
+ if self.is_blocked(modname) or self.get_plugin(modname) is not None:
+ return
+
+ importspec = "_pytest." + modname if modname in builtin_plugins else modname
+ self.rewrite_hook.mark_rewrite(importspec)
+
+ if consider_entry_points:
+ loaded = self.load_setuptools_entrypoints("pytest11", name=modname)
+ if loaded:
+ return
+
+ try:
+ if sys.version_info >= (3, 11):
+ mod = importlib.import_module(importspec)
+ else:
+ # On Python 3.10, import_module breaks
+ # testing/test_config.py::test_disable_plugin_autoload.
+ __import__(importspec)
+ mod = sys.modules[importspec]
+ except ImportError as e:
+ raise ImportError(
+ f'Error importing plugin "{modname}": {e.args[0]}'
+ ).with_traceback(e.__traceback__) from e
+
+ except Skipped as e:
+ self.skipped_plugins.append((modname, e.msg or ""))
+ else:
+ self.register(mod, modname)
+
+
+def _get_plugin_specs_as_list(
+ specs: None | types.ModuleType | str | Sequence[str],
+) -> list[str]:
+ """Parse a plugins specification into a list of plugin names."""
+ # None means empty.
+ if specs is None:
+ return []
+ # Workaround for #3899 - a submodule which happens to be called "pytest_plugins".
+ if isinstance(specs, types.ModuleType):
+ return []
+ # Comma-separated list.
+ if isinstance(specs, str):
+ return specs.split(",") if specs else []
+ # Direct specification.
+ if isinstance(specs, collections.abc.Sequence):
+ return list(specs)
+ raise UsageError(
+ f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}"
+ )
+
+
+def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
+ """Given an iterable of file names in a source distribution, return the "names" that should
+ be marked for assertion rewrite.
+
+ For example the package "pytest_mock/__init__.py" should be added as "pytest_mock" in
+ the assertion rewrite mechanism.
+
+ This function has to deal with dist-info based distributions and egg based distributions
+ (which are still very much in use for "editable" installs).
+
+ Here are the file names as seen in a dist-info based distribution:
+
+ pytest_mock/__init__.py
+ pytest_mock/_version.py
+ pytest_mock/plugin.py
+ pytest_mock.egg-info/PKG-INFO
+
+ Here are the file names as seen in an egg based distribution:
+
+ src/pytest_mock/__init__.py
+ src/pytest_mock/_version.py
+ src/pytest_mock/plugin.py
+ src/pytest_mock.egg-info/PKG-INFO
+ LICENSE
+ setup.py
+
+ We have to take in account those two distribution flavors in order to determine which
+ names should be considered for assertion rewriting.
+
+ More information:
+ https://github.com/pytest-dev/pytest-mock/issues/167
+ """
+ package_files = list(package_files)
+ seen_some = False
+ for fn in package_files:
+ is_simple_module = "/" not in fn and fn.endswith(".py")
+ is_package = fn.count("/") == 1 and fn.endswith("__init__.py")
+ if is_simple_module:
+ module_name, _ = os.path.splitext(fn)
+ # we ignore "setup.py" at the root of the distribution
+ # as well as editable installation finder modules made by setuptools
+ if module_name != "setup" and not module_name.startswith("__editable__"):
+ seen_some = True
+ yield module_name
+ elif is_package:
+ package_name = os.path.dirname(fn)
+ seen_some = True
+ yield package_name
+
+ if not seen_some:
+ # At this point we did not find any packages or modules suitable for assertion
+ # rewriting, so we try again by stripping the first path component (to account for
+ # "src" based source trees for example).
+ # This approach lets us have the common case continue to be fast, as egg-distributions
+ # are rarer.
+ new_package_files = []
+ for fn in package_files:
+ parts = fn.split("/")
+ new_fn = "/".join(parts[1:])
+ if new_fn:
+ new_package_files.append(new_fn)
+ if new_package_files:
+ yield from _iter_rewritable_modules(new_package_files)
+
+
+class _DeprecatedInicfgProxy(MutableMapping[str, Any]):
+ """Compatibility proxy for the deprecated Config.inicfg."""
+
+ __slots__ = ("_config",)
+
+ def __init__(self, config: Config) -> None:
+ self._config = config
+
+ def __getitem__(self, key: str) -> Any:
+ return self._config._inicfg[key].value
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self._config._inicfg[key] = ConfigValue(value, origin="override", mode="toml")
+
+ def __delitem__(self, key: str) -> None:
+ del self._config._inicfg[key]
+
+ def __iter__(self) -> Iterator[str]:
+ return iter(self._config._inicfg)
+
+ def __len__(self) -> int:
+ return len(self._config._inicfg)
+
+
+@final
+class Config:
+ """Access to configuration values, pluginmanager and plugin hooks.
+
+ :param PytestPluginManager pluginmanager:
+ A pytest PluginManager.
+
+ :param InvocationParams invocation_params:
+ Object containing parameters regarding the :func:`pytest.main`
+ invocation.
+ """
+
+ @final
+ @dataclasses.dataclass(frozen=True)
+ class InvocationParams:
+ """Holds parameters passed during :func:`pytest.main`.
+
+ The object attributes are read-only.
+
+ .. versionadded:: 5.1
+
+ .. note::
+
+ Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts``
+ configuration option are handled by pytest, not being included in the ``args`` attribute.
+
+ Plugins accessing ``InvocationParams`` must be aware of that.
+ """
+
+ args: tuple[str, ...]
+ """The command-line arguments as passed to :func:`pytest.main`."""
+ plugins: Sequence[str | _PluggyPlugin] | None
+ """Extra plugins, might be `None`."""
+ dir: pathlib.Path
+ """The directory from which :func:`pytest.main` was invoked."""
+
+ def __init__(
+ self,
+ *,
+ args: Iterable[str],
+ plugins: Sequence[str | _PluggyPlugin] | None,
+ dir: pathlib.Path,
+ ) -> None:
+ object.__setattr__(self, "args", tuple(args))
+ object.__setattr__(self, "plugins", plugins)
+ object.__setattr__(self, "dir", dir)
+
+ class ArgsSource(enum.Enum):
+ """Indicates the source of the test arguments.
+
+ .. versionadded:: 7.2
+ """
+
+ #: Command line arguments.
+ ARGS = enum.auto()
+ #: Invocation directory.
+ INVOCATION_DIR = enum.auto()
+ INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
+ #: 'testpaths' configuration value.
+ TESTPATHS = enum.auto()
+
+ # Set by cacheprovider plugin.
+ cache: Cache
+
+ def __init__(
+ self,
+ pluginmanager: PytestPluginManager,
+ *,
+ invocation_params: InvocationParams | None = None,
+ prog: str | None = None,
+ ) -> None:
+ if invocation_params is None:
+ invocation_params = self.InvocationParams(
+ args=(), plugins=None, dir=pathlib.Path.cwd()
+ )
+
+ self.option = argparse.Namespace()
+ """Access to command line option as attributes.
+
+ :type: argparse.Namespace
+ """
+
+ self.invocation_params = invocation_params
+ """The parameters with which pytest was invoked.
+
+ :type: InvocationParams
+ """
+
+ self._parser = Parser(
+ usage=f"%(prog)s [options] [{FILE_OR_DIR}] [{FILE_OR_DIR}] [...]",
+ processopt=self._processopt,
+ prog=prog,
+ _ispytest=True,
+ )
+ self.pluginmanager = pluginmanager
+ """The plugin manager handles plugin registration and hook invocation.
+
+ :type: PytestPluginManager
+ """
+
+ self.stash = Stash()
+ """A place where plugins can store information on the config for their
+ own use.
+
+ :type: Stash
+ """
+ # Deprecated alias. Was never public. Can be removed in a few releases.
+ self._store = self.stash
+
+ self.trace = self.pluginmanager.trace.root.get("config")
+ self.hook = self.pluginmanager.hook
+ self._inicache: dict[str, Any] = {}
+ self._inicfg: ConfigDict = {}
+ self._cleanup_stack = contextlib.ExitStack()
+ self.pluginmanager.register(self, "pytestconfig")
+ self._configured = False
+ self.hook.pytest_addoption.call_historic(
+ kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
+ )
+ self.args_source = Config.ArgsSource.ARGS
+ self.args: list[str] = []
+
+ if TYPE_CHECKING:
+
+ @deprecated(
+ "config.inicfg is deprecated, use config.getini() to access configuration values instead.",
+ )
+ @property
+ def inicfg(self) -> _DeprecatedInicfgProxy:
+ raise NotImplementedError()
+ else:
+
+ @property
+ def inicfg(self) -> _DeprecatedInicfgProxy:
+ warnings.warn(
+ _pytest.deprecated.CONFIG_INICFG,
+ stacklevel=2,
+ )
+ return _DeprecatedInicfgProxy(self)
+
+ @property
+ def rootpath(self) -> pathlib.Path:
+ """The path to the :ref:`rootdir `.
+
+ .. versionadded:: 6.1
+ """
+ return self._rootpath
+
+ @property
+ def inipath(self) -> pathlib.Path | None:
+ """The path to the :ref:`configfile `.
+
+ .. versionadded:: 6.1
+ """
+ return self._inipath
+
+ def add_cleanup(self, func: Callable[[], None]) -> None:
+ """Add a function to be called when the config object gets out of
+ use (usually coinciding with pytest_unconfigure).
+ """
+ self._cleanup_stack.callback(func)
+
+ def _do_configure(self) -> None:
+ assert not self._configured
+ self._configured = True
+ self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
+
+ def _ensure_unconfigure(self) -> None:
+ try:
+ if self._configured:
+ self._configured = False
+ try:
+ self.hook.pytest_unconfigure(config=self)
+ finally:
+ self.hook.pytest_configure._call_history = []
+ finally:
+ try:
+ self._cleanup_stack.close()
+ finally:
+ self._cleanup_stack = contextlib.ExitStack()
+
+ def get_terminal_writer(self) -> TerminalWriter:
+ terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(
+ "terminalreporter"
+ )
+ assert terminalreporter is not None
+ return terminalreporter._tw
+
+ def pytest_cmdline_parse(
+ self, pluginmanager: PytestPluginManager, args: list[str]
+ ) -> Config:
+ try:
+ self.parse(args)
+ except UsageError:
+ # Handle `--version --version` and `--help` here in a minimal fashion.
+ # This gets done via helpconfig normally, but its
+ # pytest_cmdline_main is not called in case of errors.
+ if getattr(self.option, "version", False) or "--version" in args:
+ from _pytest.helpconfig import show_version_verbose
+
+ # Note that `--version` (single argument) is handled early by `Config.main()`, so the only
+ # way we are reaching this point is via `--version --version`.
+ show_version_verbose(self)
+ elif (
+ getattr(self.option, "help", False) or "--help" in args or "-h" in args
+ ):
+ self._parser.optparser.print_help()
+ sys.stdout.write(
+ "\nNOTE: displaying only minimal help due to UsageError.\n\n"
+ )
+
+ raise
+
+ return self
+
+ def notify_exception(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ option: argparse.Namespace | None = None,
+ ) -> None:
+ if option and getattr(option, "fulltrace", False):
+ style: TracebackStyle = "long"
+ else:
+ style = "native"
+ excrepr = excinfo.getrepr(
+ funcargs=True, showlocals=getattr(option, "showlocals", False), style=style
+ )
+ res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo)
+ if not any(res):
+ for line in str(excrepr).split("\n"):
+ sys.stderr.write(f"INTERNALERROR> {line}\n")
+ sys.stderr.flush()
+
+ def cwd_relative_nodeid(self, nodeid: str) -> str:
+ # nodeid's are relative to the rootpath, compute relative to cwd.
+ if self.invocation_params.dir != self.rootpath:
+ base_path_part, *nodeid_part = nodeid.split("::")
+ # Only process path part
+ fullpath = self.rootpath / base_path_part
+ relative_path = bestrelpath(self.invocation_params.dir, fullpath)
+
+ nodeid = "::".join([relative_path, *nodeid_part])
+ return nodeid
+
+ @classmethod
+ def fromdictargs(cls, option_dict: Mapping[str, Any], args: list[str]) -> Config:
+ """Constructor usable for subprocesses."""
+ config = get_config(args)
+ config.option.__dict__.update(option_dict)
+ config.parse(args, addopts=False)
+ for x in config.option.plugins:
+ config.pluginmanager.consider_pluginarg(x)
+ return config
+
+ def _processopt(self, opt: Argument) -> None:
+ if not hasattr(self.option, opt.dest):
+ setattr(self.option, opt.dest, opt.default)
+
+ @hookimpl(trylast=True)
+ def pytest_load_initial_conftests(self, early_config: Config) -> None:
+ # We haven't fully parsed the command line arguments yet, so
+ # early_config.args it not set yet. But we need it for
+ # discovering the initial conftests. So "pre-run" the logic here.
+ # It will be done for real in `parse()`.
+ args, _args_source = early_config._decide_args(
+ args=early_config.known_args_namespace.file_or_dir,
+ pyargs=early_config.known_args_namespace.pyargs,
+ testpaths=early_config.getini("testpaths"),
+ invocation_dir=early_config.invocation_params.dir,
+ rootpath=early_config.rootpath,
+ warn=False,
+ )
+ self.pluginmanager._set_initial_conftests(
+ args=args,
+ pyargs=early_config.known_args_namespace.pyargs,
+ noconftest=early_config.known_args_namespace.noconftest,
+ rootpath=early_config.rootpath,
+ confcutdir=early_config.known_args_namespace.confcutdir,
+ invocation_dir=early_config.invocation_params.dir,
+ importmode=early_config.known_args_namespace.importmode,
+ consider_namespace_packages=early_config.getini(
+ "consider_namespace_packages"
+ ),
+ )
+
+ def _consider_importhook(self) -> None:
+ """Install the PEP 302 import hook if using assertion rewriting.
+
+ Needs to parse the --assert= option from the commandline
+ and find all the installed plugins to mark them for rewriting
+ by the importhook.
+ """
+ mode = getattr(self.known_args_namespace, "assertmode", "plain")
+
+ disable_autoload = getattr(
+ self.known_args_namespace, "disable_plugin_autoload", False
+ ) or bool(os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"))
+ if mode == "rewrite":
+ import _pytest.assertion
+
+ try:
+ hook = _pytest.assertion.install_importhook(self)
+ except SystemError:
+ mode = "plain"
+ else:
+ self._mark_plugins_for_rewrite(hook, disable_autoload)
+ self._warn_about_missing_assertion(mode)
+
+ def _mark_plugins_for_rewrite(
+ self, hook: AssertionRewritingHook, disable_autoload: bool
+ ) -> None:
+ """Given an importhook, mark for rewrite any top-level
+ modules or packages in the distribution package for
+ all pytest plugins."""
+ self.pluginmanager.rewrite_hook = hook
+
+ if disable_autoload:
+ # We don't autoload from distribution package entry points,
+ # no need to continue.
+ return
+
+ package_files = (
+ str(file)
+ for dist in importlib.metadata.distributions()
+ if any(ep.group == "pytest11" for ep in dist.entry_points)
+ for file in dist.files or []
+ )
+
+ for name in _iter_rewritable_modules(package_files):
+ hook.mark_rewrite(name)
+
+ def _configure_python_path(self) -> None:
+ # `pythonpath = a b` will set `sys.path` to `[a, b, x, y, z, ...]`
+ for path in reversed(self.getini("pythonpath")):
+ sys.path.insert(0, str(path))
+ self.add_cleanup(self._unconfigure_python_path)
+
+ def _unconfigure_python_path(self) -> None:
+ for path in self.getini("pythonpath"):
+ path_str = str(path)
+ if path_str in sys.path:
+ sys.path.remove(path_str)
+
+ def _validate_args(self, args: list[str], via: str) -> list[str]:
+ """Validate known args."""
+ self._parser.extra_info["config source"] = via
+ try:
+ self._parser.parse_known_and_unknown_args(
+ args, namespace=copy.copy(self.option)
+ )
+ finally:
+ self._parser.extra_info.pop("config source", None)
+
+ return args
+
+ def _decide_args(
+ self,
+ *,
+ args: list[str],
+ pyargs: bool,
+ testpaths: list[str],
+ invocation_dir: pathlib.Path,
+ rootpath: pathlib.Path,
+ warn: bool,
+ ) -> tuple[list[str], ArgsSource]:
+ """Decide the args (initial paths/nodeids) to use given the relevant inputs.
+
+ :param warn: Whether can issue warnings.
+
+ :returns: The args and the args source. Guaranteed to be non-empty.
+ """
+ if args:
+ source = Config.ArgsSource.ARGS
+ result = args
+ else:
+ if invocation_dir == rootpath:
+ source = Config.ArgsSource.TESTPATHS
+ if pyargs:
+ result = testpaths
+ else:
+ result = []
+ for path in testpaths:
+ result.extend(sorted(glob.iglob(path, recursive=True)))
+ if testpaths and not result:
+ if warn:
+ warning_text = (
+ "No files were found in testpaths; "
+ "consider removing or adjusting your testpaths configuration. "
+ "Searching recursively from the current directory instead."
+ )
+ self.issue_config_time_warning(
+ PytestConfigWarning(warning_text), stacklevel=3
+ )
+ else:
+ result = []
+ if not result:
+ source = Config.ArgsSource.INVOCATION_DIR
+ result = [str(invocation_dir)]
+ return result, source
+
+ @hookimpl(wrapper=True)
+ def pytest_collection(self) -> Generator[None, object, object]:
+ # Validate invalid configuration keys after collection is done so we
+ # take in account options added by late-loading conftest files.
+ try:
+ return (yield)
+ finally:
+ self._validate_config_options()
+
+ def _checkversion(self) -> None:
+ import pytest
+
+ minver = self.getini("minversion")
+ if minver:
+ # Imported lazily to improve start-up time.
+ from packaging.version import Version
+
+ if Version(minver) > Version(pytest.__version__):
+ raise pytest.UsageError(
+ f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
+ )
+
+ def _validate_config_options(self) -> None:
+ for key in sorted(self._get_unknown_ini_keys()):
+ self._warn_or_fail_if_strict(f"Unknown config option: {key}\n")
+
+ def _validate_plugins(self) -> None:
+ required_plugins = sorted(self.getini("required_plugins"))
+ if not required_plugins:
+ return
+
+ # Imported lazily to improve start-up time.
+ from packaging.requirements import InvalidRequirement
+ from packaging.requirements import Requirement
+ from packaging.version import Version
+
+ plugin_info = self.pluginmanager.list_plugin_distinfo()
+ plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
+
+ missing_plugins = []
+ for required_plugin in required_plugins:
+ try:
+ req = Requirement(required_plugin)
+ except InvalidRequirement:
+ missing_plugins.append(required_plugin)
+ continue
+
+ if req.name not in plugin_dist_info:
+ missing_plugins.append(required_plugin)
+ elif not req.specifier.contains(
+ Version(plugin_dist_info[req.name]), prereleases=True
+ ):
+ missing_plugins.append(required_plugin)
+
+ if missing_plugins:
+ raise UsageError(
+ "Missing required plugins: {}".format(", ".join(missing_plugins)),
+ )
+
+ def _warn_or_fail_if_strict(self, message: str) -> None:
+ strict_config = self.getini("strict_config")
+ if strict_config is None:
+ strict_config = self.getini("strict")
+ if strict_config:
+ raise UsageError(message)
+
+ self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3)
+
+ def _get_unknown_ini_keys(self) -> set[str]:
+ known_keys = self._parser._inidict.keys() | self._parser._ini_aliases.keys()
+ return self._inicfg.keys() - known_keys
+
+ def parse(self, args: list[str], addopts: bool = True) -> None:
+ # Parse given cmdline arguments into this config object.
+ assert self.args == [], (
+ "can only parse cmdline args at most once per Config object"
+ )
+
+ self.hook.pytest_addhooks.call_historic(
+ kwargs=dict(pluginmanager=self.pluginmanager)
+ )
+
+ if addopts:
+ env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
+ if len(env_addopts):
+ args[:] = (
+ self._validate_args(shlex.split(env_addopts), "via PYTEST_ADDOPTS")
+ + args
+ )
+
+ # At this point, self.option contains only defaults from the _processopt
+ # callback.
+ ns = self._parser.parse_known_args(args, namespace=copy.copy(self.option))
+ rootpath, inipath, inicfg, ignored_config_files = determine_setup(
+ inifile=ns.inifilename,
+ override_ini=ns.override_ini,
+ args=ns.file_or_dir,
+ rootdir_cmd_arg=ns.rootdir or None,
+ invocation_dir=self.invocation_params.dir,
+ )
+ self._rootpath = rootpath
+ self._inipath = inipath
+ self._ignored_config_files = ignored_config_files
+ self._inicfg = inicfg
+ self._parser.extra_info["rootdir"] = str(self.rootpath)
+ self._parser.extra_info["inifile"] = str(self.inipath)
+
+ self._parser.addini("addopts", "Extra command line options", "args")
+ self._parser.addini("minversion", "Minimally required pytest version")
+ self._parser.addini(
+ "pythonpath", type="paths", help="Add paths to sys.path", default=[]
+ )
+ self._parser.addini(
+ "required_plugins",
+ "Plugins that must be present for pytest to run",
+ type="args",
+ default=[],
+ )
+
+ if addopts:
+ args[:] = (
+ self._validate_args(self.getini("addopts"), "via addopts config") + args
+ )
+
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.option)
+ )
+ if addopts:
+ # addopts may have added overrides (especially via OverrideIniAction).
+ # The thing can be endlessly circular but we only do one level (#14442).
+ if overrides := parse_override_ini(self.known_args_namespace.override_ini):
+ self._inicfg.update(overrides)
+ self._inicache.clear()
+ self._checkversion()
+ self._consider_importhook()
+ self._configure_python_path()
+ self.pluginmanager.consider_preparse(args, exclude_only=False)
+ if (
+ not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD")
+ and not self.known_args_namespace.disable_plugin_autoload
+ ):
+ # Autoloading from distribution package entry point has
+ # not been disabled.
+ self.pluginmanager.load_setuptools_entrypoints("pytest11")
+ # Otherwise only plugins explicitly specified in PYTEST_PLUGINS
+ # are going to be loaded.
+ self.pluginmanager.consider_env()
+
+ # Parse again, now including options added in pytest_addoption
+ # by third-party plugins loaded above. This way they're available
+ # on early_config in the pytest_load_initial_conftests hook call below.
+ self.known_args_namespace = self._parser.parse_known_args(
+ args, namespace=copy.copy(self.option)
+ )
+
+ self._validate_plugins()
+ self._warn_about_skipped_plugins()
+
+ if self.known_args_namespace.confcutdir is None:
+ if self.inipath is not None:
+ confcutdir = str(self.inipath.parent)
+ else:
+ confcutdir = str(self.rootpath)
+ self.known_args_namespace.confcutdir = confcutdir
+ try:
+ self.hook.pytest_load_initial_conftests(
+ early_config=self, args=args, parser=self._parser
+ )
+ except ConftestImportFailure as e:
+ if self.known_args_namespace.help or self.known_args_namespace.version:
+ # we don't want to prevent --help/--version to work
+ # so just let it pass and print a warning at the end
+ self.issue_config_time_warning(
+ PytestConfigWarning(f"could not load initial conftests: {e.path}"),
+ stacklevel=2,
+ )
+ else:
+ raise
+
+ try:
+ self._parser.parse(args, namespace=self.option)
+ except PrintHelp:
+ return
+
+ self.args, self.args_source = self._decide_args(
+ args=getattr(self.option, FILE_OR_DIR),
+ pyargs=self.option.pyargs,
+ testpaths=self.getini("testpaths"),
+ invocation_dir=self.invocation_params.dir,
+ rootpath=self.rootpath,
+ warn=True,
+ )
+
+ def issue_config_time_warning(self, warning: Warning, stacklevel: int) -> None:
+ """Issue and handle a warning during the "configure" stage.
+
+ During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
+ function because it is not possible to have hook wrappers around ``pytest_configure``.
+
+ This function is mainly intended for plugins that need to issue warnings during
+ ``pytest_configure`` (or similar stages).
+
+ :param warning: The warning instance.
+ :param stacklevel: stacklevel forwarded to warnings.warn.
+ """
+ if self.pluginmanager.is_blocked("warnings"):
+ return
+
+ cmdline_filters = self.known_args_namespace.pythonwarnings or []
+ config_filters = self.getini("filterwarnings")
+
+ with warnings.catch_warnings(record=True) as records:
+ warnings.simplefilter("always", type(warning))
+ apply_warning_filters(config_filters, cmdline_filters)
+ warnings.warn(warning, stacklevel=stacklevel)
+
+ if records:
+ frame = sys._getframe(stacklevel - 1)
+ location = frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name
+ self.hook.pytest_warning_recorded.call_historic(
+ kwargs=dict(
+ warning_message=records[0],
+ when="config",
+ nodeid="",
+ location=location,
+ )
+ )
+
+ def addinivalue_line(self, name: str, line: str) -> None:
+ """Add a line to a configuration option. The option must have been
+ declared but might not yet be set in which case the line becomes
+ the first line in its value."""
+ x = self.getini(name)
+ assert isinstance(x, list)
+ x.append(line) # modifies the cached list inline
+
+ def getini(self, name: str) -> Any:
+ """Return configuration value the an :ref:`configuration file `.
+
+ If a configuration value is not defined in a
+ :ref:`configuration file `, then the ``default`` value
+ provided while registering the configuration through
+ :func:`parser.addini ` will be returned.
+ Please note that you can even provide ``None`` as a valid
+ default value.
+
+ If ``default`` is not provided while registering using
+ :func:`parser.addini `, then a default value
+ based on the ``type`` parameter passed to
+ :func:`parser.addini ` will be returned.
+ The default values based on ``type`` are:
+ ``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
+ ``bool`` : ``False``
+ ``string`` : empty string ``""``
+ ``int`` : ``0``
+ ``float`` : ``0.0``
+
+ If neither the ``default`` nor the ``type`` parameter is passed
+ while registering the configuration through
+ :func:`parser.addini `, then the configuration
+ is treated as a string and a default empty string '' is returned.
+
+ If the specified name hasn't been registered through a prior
+ :func:`parser.addini ` call (usually from a
+ plugin), a ValueError is raised.
+ """
+ canonical_name = self._parser._ini_aliases.get(name, name)
+ try:
+ return self._inicache[canonical_name]
+ except KeyError:
+ pass
+ self._inicache[canonical_name] = val = self._getini(canonical_name)
+ return val
+
+ # Meant for easy monkeypatching by legacypath plugin.
+ # Can be inlined back (with no cover removed) once legacypath is gone.
+ def _getini_unknown_type(self, name: str, type: str, value: object):
+ msg = (
+ f"Option {name} has unknown configuration type {type} with value {value!r}"
+ )
+ raise ValueError(msg) # pragma: no cover
+
+ def _getini(self, name: str):
+ # If this is an alias, resolve to canonical name.
+ canonical_name = self._parser._ini_aliases.get(name, name)
+
+ try:
+ _description, type, default = self._parser._inidict[canonical_name]
+ except KeyError as e:
+ raise ValueError(f"unknown configuration value: {name!r}") from e
+
+ # Collect all possible values (canonical name + aliases) from _inicfg.
+ # Each candidate is (ConfigValue, is_canonical).
+ candidates = []
+ if canonical_name in self._inicfg:
+ candidates.append((self._inicfg[canonical_name], True))
+ for alias, target in self._parser._ini_aliases.items():
+ if target == canonical_name and alias in self._inicfg:
+ candidates.append((self._inicfg[alias], False))
+
+ if not candidates:
+ return default
+
+ # Pick the best candidate based on precedence:
+ # 1. CLI override takes precedence over file, then
+ # 2. Canonical name takes precedence over alias.
+ selected = max(candidates, key=lambda x: (x[0].origin == "override", x[1]))[0]
+ value = selected.value
+ mode = selected.mode
+
+ if mode == "ini":
+ # In ini mode, values are always str | list[str].
+ assert isinstance(value, (str, list))
+ return self._getini_ini(name, canonical_name, type, value, default)
+ elif mode == "toml":
+ return self._getini_toml(name, canonical_name, type, value, default)
+ else:
+ assert_never(mode)
+
+ def _getini_ini(
+ self,
+ name: str,
+ canonical_name: str,
+ type: str,
+ value: str | list[str],
+ default: Any,
+ ):
+ """Handle config values read in INI mode.
+
+ In INI mode, values are stored as str or list[str] only, and coerced
+ from string based on the registered type.
+ """
+ # Note: some coercions are only required if we are reading from .ini
+ # files, because the file format doesn't contain type information, but
+ # when reading from toml (in ini mode) we will get either str or list of
+ # str values (see load_config_dict_from_file). For example:
+ #
+ # ini:
+ # a_line_list = "tests acceptance"
+ #
+ # in this case, we need to split the string to obtain a list of strings.
+ #
+ # toml (ini mode):
+ # a_line_list = ["tests", "acceptance"]
+ #
+ # in this case, we already have a list ready to use.
+ if type == "paths":
+ dp = (
+ self.inipath.parent
+ if self.inipath is not None
+ else self.invocation_params.dir
+ )
+ input_values = shlex.split(value) if isinstance(value, str) else value
+ return [dp / x for x in input_values]
+ elif type == "args":
+ return shlex.split(value) if isinstance(value, str) else value
+ elif type == "linelist":
+ if isinstance(value, str):
+ return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
+ else:
+ return value
+ elif type == "bool":
+ return _strtobool(str(value).strip())
+ elif type == "string":
+ return value
+ elif type == "int":
+ if not isinstance(value, str):
+ raise TypeError(
+ f"Expected an int string for option {name} of type integer, but got: {value!r}"
+ ) from None
+ return int(value)
+ elif type == "float":
+ if not isinstance(value, str):
+ raise TypeError(
+ f"Expected a float string for option {name} of type float, but got: {value!r}"
+ ) from None
+ return float(value)
+ else:
+ return self._getini_unknown_type(name, type, value)
+
+ def _getini_toml(
+ self,
+ name: str,
+ canonical_name: str,
+ type: str,
+ value: object,
+ default: Any,
+ ):
+ """Handle TOML config values with strict type validation and no coercion.
+
+ In TOML mode, values already have native types from TOML parsing.
+ We validate types match expectations exactly, including list items.
+ """
+ value_type = builtins.type(value).__name__
+ if type == "paths":
+ # Expect a list of strings.
+ if not isinstance(value, list):
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects a list for type 'paths', "
+ f"got {value_type}: {value!r}"
+ )
+ for i, item in enumerate(value):
+ if not isinstance(item, str):
+ item_type = builtins.type(item).__name__
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects a list of strings, "
+ f"but item at index {i} is {item_type}: {item!r}"
+ )
+ dp = (
+ self.inipath.parent
+ if self.inipath is not None
+ else self.invocation_params.dir
+ )
+ return [dp / x for x in value]
+ elif type in {"args", "linelist"}:
+ # Expect a list of strings.
+ if not isinstance(value, list):
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects a list for type '{type}', "
+ f"got {value_type}: {value!r}"
+ )
+ for i, item in enumerate(value):
+ if not isinstance(item, str):
+ item_type = builtins.type(item).__name__
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects a list of strings, "
+ f"but item at index {i} is {item_type}: {item!r}"
+ )
+ return list(value)
+ elif type == "bool":
+ # Expect a boolean.
+ if not isinstance(value, bool):
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects a bool, "
+ f"got {value_type}: {value!r}"
+ )
+ return value
+ elif type == "int":
+ # Expect an integer (but not bool, which is a subclass of int).
+ if not isinstance(value, int) or isinstance(value, bool):
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects an int, "
+ f"got {value_type}: {value!r}"
+ )
+ return value
+ elif type == "float":
+ # Expect a float or integer only.
+ if not isinstance(value, (float, int)) or isinstance(value, bool):
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects a float, "
+ f"got {value_type}: {value!r}"
+ )
+ return value
+ elif type == "string":
+ # Expect a string.
+ if not isinstance(value, str):
+ raise TypeError(
+ f"{self.inipath}: config option '{name}' expects a string, "
+ f"got {value_type}: {value!r}"
+ )
+ return value
+ else:
+ return self._getini_unknown_type(name, type, value)
+
+ def _getconftest_pathlist(
+ self, name: str, path: pathlib.Path
+ ) -> list[pathlib.Path] | None:
+ try:
+ mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
+ except KeyError:
+ return None
+ assert mod.__file__ is not None
+ modpath = pathlib.Path(mod.__file__).parent
+ values: list[pathlib.Path] = []
+ for relroot in relroots:
+ if isinstance(relroot, os.PathLike):
+ relroot = pathlib.Path(relroot)
+ else:
+ relroot = relroot.replace("/", os.sep)
+ relroot = absolutepath(modpath / relroot)
+ values.append(relroot)
+ return values
+
+ def getoption(self, name: str, default: Any = NOTSET, skip: bool = False):
+ """Return command line option value.
+
+ :param name: Name of the option. You may also specify
+ the literal ``--OPT`` option instead of the "dest" option name.
+ :param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`.
+ Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``.
+ :param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value.
+ Note that even if ``True``, if a default was specified it will be returned instead of a skip.
+ """
+ name = self._parser._opt2dest.get(name, name)
+ try:
+ val = getattr(self.option, name)
+ if val is None and skip:
+ raise AttributeError(name)
+ return val
+ except AttributeError as e:
+ if default is not NOTSET:
+ return default
+ if skip:
+ import pytest
+
+ pytest.skip(f"no {name!r} option found")
+ raise ValueError(f"no option named {name!r}") from e
+
+ def getvalue(self, name: str, path=None):
+ """Deprecated, use getoption() instead."""
+ return self.getoption(name)
+
+ def getvalueorskip(self, name: str, path=None):
+ """Deprecated, use getoption(skip=True) instead."""
+ return self.getoption(name, skip=True)
+
+ #: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
+ VERBOSITY_ASSERTIONS: Final = "assertions"
+ #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
+ VERBOSITY_TEST_CASES: Final = "test_cases"
+ #: Verbosity type for failed subtests (see :confval:`verbosity_subtests`).
+ VERBOSITY_SUBTESTS: Final = "subtests"
+
+ _VERBOSITY_INI_DEFAULT: Final = "auto"
+
+ def get_verbosity(self, verbosity_type: str | None = None) -> int:
+ r"""Retrieve the verbosity level for a fine-grained verbosity type.
+
+ :param verbosity_type: Verbosity type to get level for. If a level is
+ configured for the given type, that value will be returned. If the
+ given type is not a known verbosity type, the global verbosity
+ level will be returned. If the given type is None (default), the
+ global verbosity level will be returned.
+
+ To configure a level for a fine-grained verbosity type, the
+ configuration file should have a setting for the configuration name
+ and a numeric value for the verbosity level. A special value of "auto"
+ can be used to explicitly use the global verbosity level.
+
+ Example:
+
+ .. tab:: toml
+
+ .. code-block:: toml
+
+ [tool.pytest]
+ verbosity_assertions = 2
+
+ .. tab:: ini
+
+ .. code-block:: ini
+
+ [pytest]
+ verbosity_assertions = 2
+
+ .. code-block:: console
+
+ pytest -v
+
+ .. code-block:: python
+
+ print(config.get_verbosity()) # 1
+ print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
+ """
+ global_level = self.getoption("verbose", default=0)
+ assert isinstance(global_level, int)
+ if verbosity_type is None:
+ return global_level
+
+ ini_name = Config._verbosity_ini_name(verbosity_type)
+ if ini_name not in self._parser._inidict:
+ return global_level
+
+ level = self.getini(ini_name)
+ if level == Config._VERBOSITY_INI_DEFAULT:
+ return global_level
+
+ return int(level)
+
+ @staticmethod
+ def _verbosity_ini_name(verbosity_type: str) -> str:
+ return f"verbosity_{verbosity_type}"
+
+ @staticmethod
+ def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None:
+ """Add a output verbosity configuration option for the given output type.
+
+ :param parser: Parser for command line arguments and config-file values.
+ :param verbosity_type: Fine-grained verbosity category.
+ :param help: Description of the output this type controls.
+
+ The value should be retrieved via a call to
+ :py:func:`config.get_verbosity(type) `.
+ """
+ parser.addini(
+ Config._verbosity_ini_name(verbosity_type),
+ help=help,
+ type="string",
+ default=Config._VERBOSITY_INI_DEFAULT,
+ )
+
+ def _warn_about_missing_assertion(self, mode: str) -> None:
+ if not _assertion_supported():
+ if mode == "plain":
+ warning_text = (
+ "ASSERTIONS ARE NOT EXECUTED"
+ " and FAILING TESTS WILL PASS. Are you"
+ " using python -O?"
+ )
+ else:
+ warning_text = (
+ "assertions not in test modules or"
+ " plugins will be ignored"
+ " because assert statements are not executed "
+ "by the underlying Python interpreter "
+ "(are you using python -O?)\n"
+ )
+ self.issue_config_time_warning(
+ PytestConfigWarning(warning_text),
+ stacklevel=3,
+ )
+
+ def _warn_about_skipped_plugins(self) -> None:
+ for module_name, msg in self.pluginmanager.skipped_plugins:
+ self.issue_config_time_warning(
+ PytestConfigWarning(f"skipped plugin {module_name!r}: {msg}"),
+ stacklevel=2,
+ )
+
+
+def _assertion_supported() -> bool:
+ try:
+ assert False
+ except AssertionError:
+ return True
+ else:
+ return False # type: ignore[unreachable]
+
+
+def create_terminal_writer(
+ config: Config, file: TextIO | None = None
+) -> TerminalWriter:
+ """Create a TerminalWriter instance configured according to the options
+ in the config object.
+
+ Every code which requires a TerminalWriter object and has access to a
+ config object should use this function.
+ """
+ tw = TerminalWriter(file=file)
+
+ if config.option.color == "yes":
+ tw.hasmarkup = True
+ elif config.option.color == "no":
+ tw.hasmarkup = False
+
+ if config.option.code_highlight == "yes":
+ tw.code_highlight = True
+ elif config.option.code_highlight == "no":
+ tw.code_highlight = False
+
+ return tw
+
+
+def _strtobool(val: str) -> bool:
+ """Convert a string representation of truth to True or False.
+
+ True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
+ are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
+ 'val' is anything else.
+
+ .. note:: Copied from distutils.util.
+ """
+ val = val.lower()
+ if val in ("y", "yes", "t", "true", "on", "1"):
+ return True
+ elif val in ("n", "no", "f", "false", "off", "0"):
+ return False
+ else:
+ raise ValueError(f"invalid truth value {val!r}")
+
+
+@lru_cache(maxsize=50)
+def parse_warning_filter(
+ arg: str, *, escape: bool
+) -> tuple[warnings._ActionKind, str, type[Warning], str, int]:
+ """Parse a warnings filter string.
+
+ This is copied from warnings._setoption with the following changes:
+
+ * Does not apply the filter.
+ * Escaping is optional.
+ * Raises UsageError so we get nice error messages on failure.
+ """
+ __tracebackhide__ = True
+ error_template = dedent(
+ f"""\
+ while parsing the following warning configuration:
+
+ {arg}
+
+ This error occurred:
+
+ {{error}}
+ """
+ )
+
+ parts = arg.split(":")
+ if len(parts) > 5:
+ doc_url = (
+ "https://docs.python.org/3/library/warnings.html#describing-warning-filters"
+ )
+ error = dedent(
+ f"""\
+ Too many fields ({len(parts)}), expected at most 5 separated by colons:
+
+ action:message:category:module:line
+
+ For more information please consult: {doc_url}
+ """
+ )
+ raise UsageError(error_template.format(error=error))
+
+ while len(parts) < 5:
+ parts.append("")
+ action_, message, category_, module, lineno_ = (s.strip() for s in parts)
+ try:
+ action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined]
+ except warnings._OptionError as e:
+ raise UsageError(error_template.format(error=str(e))) from None
+ try:
+ category: type[Warning] = _resolve_warning_category(category_)
+ except ImportError:
+ raise
+ except Exception:
+ exc_info = ExceptionInfo.from_current()
+ exception_text = exc_info.getrepr(style="native")
+ raise UsageError(error_template.format(error=exception_text)) from None
+ if message and escape:
+ message = re.escape(message)
+ if module and escape:
+ module = re.escape(module) + r"\Z"
+ if lineno_:
+ try:
+ lineno = int(lineno_)
+ if lineno < 0:
+ raise ValueError("number is negative")
+ except ValueError as e:
+ raise UsageError(
+ error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
+ ) from None
+ else:
+ lineno = 0
+ try:
+ re.compile(message)
+ re.compile(module)
+ except re.error as e:
+ raise UsageError(
+ error_template.format(error=f"Invalid regex {e.pattern!r}: {e}")
+ ) from None
+ return action, message, category, module, lineno
+
+
+def _resolve_warning_category(category: str) -> type[Warning]:
+ """
+ Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors)
+ propagate so we can get access to their tracebacks (#9218).
+ """
+ __tracebackhide__ = True
+ if not category:
+ return Warning
+
+ if "." not in category:
+ import builtins as m
+
+ klass = category
+ else:
+ module, _, klass = category.rpartition(".")
+ m = importlib.import_module(module)
+ cat = getattr(m, klass)
+ if not issubclass(cat, Warning):
+ raise UsageError(f"{cat} is not a Warning subclass")
+ return cast(type[Warning], cat)
+
+
+def apply_warning_filters(
+ config_filters: Iterable[str], cmdline_filters: Iterable[str]
+) -> None:
+ """Applies pytest-configured filters to the warnings module"""
+ # Filters should have this precedence: cmdline options, config.
+ # Filters should be applied in the inverse order of precedence.
+ for arg in config_filters:
+ try:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
+ except ImportError as e:
+ warnings.warn(
+ f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
+ )
+ continue
+
+ for arg in cmdline_filters:
+ try:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=True))
+ except ImportError as e:
+ warnings.warn(
+ f"Failed to import filter module '{e.name}': {arg}", PytestConfigWarning
+ )
+ continue
diff --git a/venv/lib/python3.11/site-packages/_pytest/config/argparsing.py b/venv/lib/python3.11/site-packages/_pytest/config/argparsing.py
new file mode 100644
index 000000000..f70e27614
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/config/argparsing.py
@@ -0,0 +1,503 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+import argparse
+from collections.abc import Callable
+from collections.abc import Sequence
+import os
+import sys
+import textwrap
+from typing import Any
+from typing import final
+from typing import Literal
+from typing import NoReturn
+
+from .exceptions import UsageError
+import _pytest._io
+from _pytest.compat import NOTSET
+from _pytest.deprecated import check_ispytest
+
+
+FILE_OR_DIR = "file_or_dir"
+
+
+@final
+class Parser:
+ """Parser for command line arguments and config-file values.
+
+ :ivar extra_info: Dict of generic param -> value to display in case
+ there's an error processing the command line arguments.
+ """
+
+ def __init__(
+ self,
+ usage: str | None = None,
+ processopt: Callable[[Argument], None] | None = None,
+ *,
+ prog: str | None = None,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+
+ from _pytest._argcomplete import filescompleter
+
+ self._processopt = processopt
+ self.extra_info: dict[str, Any] = {}
+ self.optparser = PytestArgumentParser(usage, self.extra_info, prog=prog)
+ anonymous_arggroup = self.optparser.add_argument_group("Custom options")
+ self._anonymous = OptionGroup(
+ anonymous_arggroup, "_anonymous", self, _ispytest=True
+ )
+ self._groups = [self._anonymous]
+ # Maps option strings -> dest, e.g. "-V" and "--version" to "version".
+ self._opt2dest: dict[str, str] = {}
+ file_or_dir_arg = self.optparser.add_argument(FILE_OR_DIR, nargs="*")
+ file_or_dir_arg.completer = filescompleter # type: ignore
+
+ self._inidict: dict[str, tuple[str, str, Any]] = {}
+ # Maps alias -> canonical name.
+ self._ini_aliases: dict[str, str] = {}
+
+ @property
+ def prog(self) -> str:
+ return self.optparser.prog
+
+ @prog.setter
+ def prog(self, value: str) -> None:
+ self.optparser.prog = value
+
+ def processoption(self, option: Argument) -> None:
+ if self._processopt:
+ if option.dest:
+ self._processopt(option)
+
+ def getgroup(
+ self, name: str, description: str = "", after: str | None = None
+ ) -> OptionGroup:
+ """Get (or create) a named option Group.
+
+ :param name: Name of the option group.
+ :param description: Long description for --help output.
+ :param after: Name of another group, used for ordering --help output.
+ :returns: The option group.
+
+ The returned group object has an ``addoption`` method with the same
+ signature as :func:`parser.addoption ` but
+ will be shown in the respective group in the output of
+ ``pytest --help``.
+ """
+ for group in self._groups:
+ if group.name == name:
+ return group
+
+ arggroup = self.optparser.add_argument_group(description or name)
+ group = OptionGroup(arggroup, name, self, _ispytest=True)
+ i = 0
+ for i, grp in enumerate(self._groups):
+ if grp.name == after:
+ break
+ self._groups.insert(i + 1, group)
+ # argparse doesn't provide a way to control `--help` order, so must
+ # access its internals ☹.
+ self.optparser._action_groups.insert(i + 1, self.optparser._action_groups.pop())
+ return group
+
+ def addoption(self, *opts: str, **attrs: Any) -> None:
+ """Register a command line option.
+
+ :param opts:
+ Option names, can be short or long options.
+ :param attrs:
+ Same attributes as the argparse library's :meth:`add_argument()
+ ` function accepts.
+
+ After command line parsing, options are available on the pytest config
+ object via ``config.option.NAME`` where ``NAME`` is usually set
+ by passing a ``dest`` attribute, for example
+ ``addoption("--long", dest="NAME", ...)``.
+ """
+ self._anonymous.addoption(*opts, **attrs)
+
+ def parse(
+ self,
+ args: Sequence[str | os.PathLike[str]],
+ namespace: argparse.Namespace | None = None,
+ ) -> argparse.Namespace:
+ """Parse the arguments.
+
+ Unlike ``parse_known_args`` and ``parse_known_and_unknown_args``,
+ raises PrintHelp on `--help` and UsageError on unknown flags
+
+ :meta private:
+ """
+ from _pytest._argcomplete import try_argcomplete
+
+ try_argcomplete(self.optparser)
+ strargs = [os.fspath(x) for x in args]
+ if namespace is None:
+ namespace = argparse.Namespace()
+ try:
+ namespace._raise_print_help = True
+ return self.optparser.parse_intermixed_args(strargs, namespace=namespace)
+ finally:
+ del namespace._raise_print_help
+
+ def parse_known_args(
+ self,
+ args: Sequence[str | os.PathLike[str]],
+ namespace: argparse.Namespace | None = None,
+ ) -> argparse.Namespace:
+ """Parse the known arguments at this point.
+
+ :returns: An argparse namespace object.
+ """
+ return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
+
+ def parse_known_and_unknown_args(
+ self,
+ args: Sequence[str | os.PathLike[str]],
+ namespace: argparse.Namespace | None = None,
+ ) -> tuple[argparse.Namespace, list[str]]:
+ """Parse the known arguments at this point, and also return the
+ remaining unknown flag arguments.
+
+ :returns:
+ A tuple containing an argparse namespace object for the known
+ arguments, and a list of unknown flag arguments.
+ """
+ strargs = [os.fspath(x) for x in args]
+ if sys.version_info < (3, 12, 8) or (3, 13) <= sys.version_info < (3, 13, 1):
+ # Older argparse have a bugged parse_known_intermixed_args.
+ namespace, unknown = self.optparser.parse_known_args(strargs, namespace)
+ assert namespace is not None
+ file_or_dir = getattr(namespace, FILE_OR_DIR)
+ unknown_flags: list[str] = []
+ for arg in unknown:
+ (unknown_flags if arg.startswith("-") else file_or_dir).append(arg)
+ return namespace, unknown_flags
+ else:
+ return self.optparser.parse_known_intermixed_args(strargs, namespace)
+
+ def addini(
+ self,
+ name: str,
+ help: str,
+ type: Literal[
+ "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
+ ]
+ | None = None,
+ default: Any = NOTSET,
+ *,
+ aliases: Sequence[str] = (),
+ ) -> None:
+ """Register a configuration file option.
+
+ :param name:
+ Name of the configuration.
+ :param type:
+ Type of the configuration. Can be:
+
+ * ``string``: a string
+ * ``bool``: a boolean
+ * ``args``: a list of strings, separated as in a shell
+ * ``linelist``: a list of strings, separated by line breaks
+ * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
+ * ``pathlist``: a list of ``py.path``, separated as in a shell
+ * ``int``: an integer
+ * ``float``: a floating-point number
+
+ .. versionadded:: 8.4
+
+ The ``float`` and ``int`` types.
+
+ For ``paths`` and ``pathlist`` types, they are considered relative to the config-file.
+ In case the execution is happening without a config-file defined,
+ they will be considered relative to the current working directory (for example with ``--override-ini``).
+
+ .. versionadded:: 7.0
+ The ``paths`` variable type.
+
+ .. versionadded:: 8.1
+ Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of a config-file.
+
+ Defaults to ``string`` if ``None`` or not passed.
+ :param default:
+ Default value if no config-file option exists but is queried.
+ :param aliases:
+ Additional names by which this option can be referenced.
+ Aliases resolve to the canonical name.
+
+ .. versionadded:: 9.0
+ The ``aliases`` parameter.
+
+ The value of configuration keys can be retrieved via a call to
+ :py:func:`config.getini(name) `.
+ """
+ assert type in (
+ None,
+ "string",
+ "paths",
+ "pathlist",
+ "args",
+ "linelist",
+ "bool",
+ "int",
+ "float",
+ )
+ if type is None:
+ type = "string"
+ if default is NOTSET:
+ default = get_ini_default_for_type(type)
+
+ self._inidict[name] = (help, type, default)
+
+ for alias in aliases:
+ if alias in self._inidict:
+ raise ValueError(
+ f"alias {alias!r} conflicts with existing configuration option"
+ )
+ if (already := self._ini_aliases.get(alias)) is not None:
+ raise ValueError(f"{alias!r} is already an alias of {already!r}")
+ self._ini_aliases[alias] = name
+
+
+def get_ini_default_for_type(
+ type: Literal[
+ "string", "paths", "pathlist", "args", "linelist", "bool", "int", "float"
+ ],
+) -> Any:
+ """
+ Used by addini to get the default value for a given config option type, when
+ default is not supplied.
+ """
+ if type in ("paths", "pathlist", "args", "linelist"):
+ return []
+ elif type == "bool":
+ return False
+ elif type == "int":
+ return 0
+ elif type == "float":
+ return 0.0
+ else:
+ return ""
+
+
+class Argument:
+ """An option defined in an OptionGroup."""
+
+ def __init__(self, action: argparse.Action) -> None:
+ self._action = action
+
+ def attrs(self) -> dict[str, Any]:
+ return self._action.__dict__
+
+ def names(self) -> Sequence[str]:
+ return self._action.option_strings
+
+ @property
+ def dest(self) -> str:
+ return self._action.dest
+
+ @property
+ def default(self) -> Any:
+ return self._action.default
+
+ @property
+ def type(self) -> Any | None:
+ return self._action.type
+
+ def __repr__(self) -> str:
+ action = getattr(self, "_action", None)
+ if action is None:
+ return "Argument()"
+ args: list[str] = []
+ args += ["opts: " + repr(self.names())]
+ args += ["dest: " + repr(self.dest)]
+ if action.type:
+ args += ["type: " + repr(self.type)]
+ args += ["default: " + repr(self.default)]
+ return "Argument({})".format(", ".join(args))
+
+
+class OptionGroup:
+ """A group of options shown in its own section."""
+
+ def __init__(
+ self,
+ arggroup: argparse._ArgumentGroup,
+ name: str,
+ parser: Parser | None,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._arggroup = arggroup
+ self.name = name
+ self.options: list[Argument] = []
+ self.parser = parser
+
+ def addoption(self, *opts: str, **attrs: Any) -> None:
+ """Add an option to this group.
+
+ If a shortened version of a long option is specified, it will
+ be suppressed in the help. ``addoption('--twowords', '--two-words')``
+ results in help showing ``--two-words`` only, but ``--twowords`` gets
+ accepted **and** the automatic destination is in ``args.twowords``.
+
+ :param opts:
+ Option names, can be short or long options.
+ Note that lower-case short options (e.g. `-x`) are reserved.
+ :param attrs:
+ Same attributes as the argparse library's :meth:`add_argument()
+ ` function accepts.
+ """
+ conflict = set(opts).intersection(
+ name for opt in self.options for name in opt.names()
+ )
+ if conflict:
+ raise ValueError(f"option names {conflict} already added")
+ self._addoption_inner(opts, attrs, allow_reserved=False)
+
+ def _addoption(self, *opts: str, **attrs: Any) -> None:
+ """Like addoption(), but also allows registering short lower case options (e.g. -x),
+ which are reserved for pytest core."""
+ self._addoption_inner(opts, attrs, allow_reserved=True)
+
+ def _addoption_inner(
+ self, opts: tuple[str, ...], attrs: dict[str, Any], allow_reserved: bool
+ ) -> None:
+ if not allow_reserved:
+ for opt in opts:
+ if len(opt) >= 2 and opt[0] == "-" and opt[1].islower():
+ raise ValueError("lowercase short options are reserved")
+
+ action = self._arggroup.add_argument(*opts, **attrs)
+ option = Argument(action)
+ self.options.append(option)
+ if self.parser:
+ for name in option.names():
+ self.parser._opt2dest[name] = option.dest
+ self.parser.processoption(option)
+
+
+class PytestArgumentParser(argparse.ArgumentParser):
+ def __init__(
+ self,
+ usage: str | None,
+ extra_info: dict[str, str],
+ *,
+ prog: str | None = None,
+ ) -> None:
+ super().__init__(
+ usage=usage,
+ prog=prog,
+ add_help=False,
+ formatter_class=DropShorterLongHelpFormatter,
+ allow_abbrev=False,
+ fromfile_prefix_chars="@",
+ )
+ # extra_info is a dict of (param -> value) to display if there's
+ # an usage error to provide more contextual information to the user.
+ self.extra_info = extra_info
+
+ def error(self, message: str) -> NoReturn:
+ """Transform argparse error message into UsageError."""
+ # TODO(py313): Replace with `exit_on_error=False`. Note that while it
+ # was added in Python 3.9, it was broken until 3.13 (cpython#121018).
+ msg = f"{self.prog}: error: {message}"
+ if self.extra_info:
+ msg += "\n" + "\n".join(
+ f" {k}: {v}" for k, v in sorted(self.extra_info.items())
+ )
+ raise UsageError(self.format_usage() + msg)
+
+
+class DropShorterLongHelpFormatter(argparse.HelpFormatter):
+ """Shorten help for long options that differ only in extra hyphens.
+
+ - Collapse **long** options that are the same except for extra hyphens.
+ - Shortcut if there are only two options and one of them is a short one.
+ """
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ # Use more accurate terminal width.
+ if "width" not in kwargs:
+ kwargs["width"] = _pytest._io.get_terminal_width()
+ super().__init__(*args, **kwargs)
+
+ def _format_action_invocation(self, action: argparse.Action) -> str:
+ orgstr = super()._format_action_invocation(action)
+ if orgstr and orgstr[0] != "-": # only optional arguments
+ return orgstr
+ options = orgstr.split(", ")
+ if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
+ # a shortcut for '-h, --help' or '--abc', '-a'
+ return orgstr
+ return_list = []
+ short_long: dict[str, str] = {}
+ for option in options:
+ if len(option) == 2 or option[2] == " ":
+ continue
+ assert option.startswith("--"), (
+ f'long optional argument without "--": [{option}]'
+ )
+ xxoption = option[2:]
+ shortened = xxoption.replace("-", "")
+ if shortened not in short_long or len(short_long[shortened]) < len(
+ xxoption
+ ):
+ short_long[shortened] = xxoption
+ # now short_long has been filled out to the longest with dashes
+ # **and** we keep the right option ordering from add_argument
+ for option in options:
+ if len(option) == 2 or option[2] == " ":
+ return_list.append(option)
+ if option[2:] == short_long.get(option.replace("-", "")):
+ return_list.append(option.replace(" ", "=", 1))
+ return ", ".join(return_list)
+
+ def _split_lines(self, text: str, width: int) -> list[str]:
+ """Wrap lines after splitting on original newlines.
+
+ This allows to have explicit line breaks in the help text.
+ """
+ lines = []
+ for line in text.splitlines():
+ lines.extend(textwrap.wrap(line.strip(), width))
+ return lines
+
+
+class OverrideIniAction(argparse.Action):
+ """Custom argparse action that makes a CLI flag equivalent to overriding an
+ option, in addition to behaving like `store_true`.
+
+ This can simplify things since code only needs to inspect the config option
+ and not consider the CLI flag.
+ """
+
+ def __init__(
+ self,
+ option_strings: Sequence[str],
+ dest: str,
+ nargs: int | str | None = None,
+ *args,
+ ini_option: str,
+ ini_value: str,
+ **kwargs,
+ ) -> None:
+ super().__init__(option_strings, dest, 0, *args, **kwargs)
+ self.ini_option = ini_option
+ self.ini_value = ini_value
+
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ *args,
+ **kwargs,
+ ) -> None:
+ setattr(namespace, self.dest, True)
+ current_overrides = getattr(namespace, "override_ini", None)
+ if current_overrides is None:
+ current_overrides = []
+ current_overrides.append(f"{self.ini_option}={self.ini_value}")
+ setattr(namespace, "override_ini", current_overrides)
diff --git a/venv/lib/python3.11/site-packages/_pytest/config/exceptions.py b/venv/lib/python3.11/site-packages/_pytest/config/exceptions.py
new file mode 100644
index 000000000..d84a9ea67
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/config/exceptions.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from typing import final
+
+
+@final
+class UsageError(Exception):
+ """Error in pytest usage or invocation."""
+
+ __module__ = "pytest"
+
+
+class PrintHelp(Exception):
+ """Raised when pytest should print its help to skip the rest of the
+ argument parsing and validation."""
diff --git a/venv/lib/python3.11/site-packages/_pytest/config/findpaths.py b/venv/lib/python3.11/site-packages/_pytest/config/findpaths.py
new file mode 100644
index 000000000..e74546c6e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/config/findpaths.py
@@ -0,0 +1,350 @@
+from __future__ import annotations
+
+from collections.abc import Iterable
+from collections.abc import Sequence
+from dataclasses import dataclass
+from dataclasses import KW_ONLY
+import os
+from pathlib import Path
+import sys
+from typing import Literal
+from typing import TypeAlias
+
+import iniconfig
+
+from .exceptions import UsageError
+from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import commonpath
+from _pytest.pathlib import safe_exists
+
+
+@dataclass(frozen=True)
+class ConfigValue:
+ """Represents a configuration value with its origin and parsing mode.
+
+ This allows tracking whether a value came from a configuration file
+ or from a CLI override (--override-ini), which is important for
+ determining precedence when dealing with ini option aliases.
+
+ The mode tracks the parsing mode/data model used for the value:
+ - "ini": from INI files or [tool.pytest.ini_options], where the only
+ supported value types are `str` or `list[str]`.
+ - "toml": from TOML files (not in INI mode), where native TOML types
+ are preserved.
+ """
+
+ value: object
+ _: KW_ONLY
+ origin: Literal["file", "override"]
+ mode: Literal["ini", "toml"]
+
+
+ConfigDict: TypeAlias = dict[str, ConfigValue]
+
+
+def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
+ """Parse the given generic '.ini' file using legacy IniConfig parser, returning
+ the parsed object.
+
+ Raise UsageError if the file cannot be parsed.
+ """
+ try:
+ return iniconfig.IniConfig(str(path))
+ except iniconfig.ParseError as exc:
+ raise UsageError(str(exc)) from exc
+
+
+def load_config_dict_from_file(
+ filepath: Path,
+) -> ConfigDict | None:
+ """Load pytest configuration from the given file path, if supported.
+
+ Return None if the file does not contain valid pytest configuration.
+ """
+ # Configuration from ini files are obtained from the [pytest] section, if present.
+ if filepath.suffix == ".ini":
+ iniconfig = _parse_ini_config(filepath)
+
+ if "pytest" in iniconfig:
+ return {
+ k: ConfigValue(v, origin="file", mode="ini")
+ for k, v in iniconfig["pytest"].items()
+ }
+ else:
+ # "pytest.ini" files are always the source of configuration, even if empty.
+ if filepath.name in {"pytest.ini", ".pytest.ini"}:
+ return {}
+
+ # '.cfg' files are considered if they contain a "[tool:pytest]" section.
+ elif filepath.suffix == ".cfg":
+ iniconfig = _parse_ini_config(filepath)
+
+ if "tool:pytest" in iniconfig.sections:
+ return {
+ k: ConfigValue(v, origin="file", mode="ini")
+ for k, v in iniconfig["tool:pytest"].items()
+ }
+ elif "pytest" in iniconfig.sections:
+ # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
+ # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
+ fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
+
+ # '.toml' files are considered if they contain a [tool.pytest] table (toml mode)
+ # or [tool.pytest.ini_options] table (ini mode) for pyproject.toml,
+ # or [pytest] table (toml mode) for pytest.toml/.pytest.toml.
+ elif filepath.suffix == ".toml":
+ if sys.version_info >= (3, 11):
+ import tomllib
+ else:
+ import tomli as tomllib
+
+ toml_text = filepath.read_text(encoding="utf-8")
+ try:
+ config = tomllib.loads(toml_text)
+ except tomllib.TOMLDecodeError as exc:
+ raise UsageError(f"{filepath}: {exc}") from exc
+
+ # pytest.toml and .pytest.toml use [pytest] table directly.
+ if filepath.name in ("pytest.toml", ".pytest.toml"):
+ pytest_config = config.get("pytest", {})
+ if pytest_config:
+ # TOML mode - preserve native TOML types.
+ return {
+ k: ConfigValue(v, origin="file", mode="toml")
+ for k, v in pytest_config.items()
+ }
+ # "pytest.toml" files are always the source of configuration, even if empty.
+ return {}
+
+ # pyproject.toml uses [tool.pytest] or [tool.pytest.ini_options].
+ else:
+ tool_pytest = config.get("tool", {}).get("pytest", {})
+
+ # Check for toml mode config: [tool.pytest] with content outside of ini_options.
+ toml_config = {k: v for k, v in tool_pytest.items() if k != "ini_options"}
+ # Check for ini mode config: [tool.pytest.ini_options].
+ ini_config = tool_pytest.get("ini_options", None)
+
+ if toml_config and ini_config:
+ raise UsageError(
+ f"{filepath}: Cannot use both [tool.pytest] (native TOML types) and "
+ "[tool.pytest.ini_options] (string-based INI format) simultaneously. "
+ "Please use [tool.pytest] with native TOML types (recommended) "
+ "or [tool.pytest.ini_options] for backwards compatibility."
+ )
+
+ if toml_config:
+ # TOML mode - preserve native TOML types.
+ return {
+ k: ConfigValue(v, origin="file", mode="toml")
+ for k, v in toml_config.items()
+ }
+
+ elif ini_config is not None:
+ # INI mode - TOML supports richer data types than INI files, but we need to
+ # convert all scalar values to str for compatibility with the INI system.
+ def make_scalar(v: object) -> str | list[str]:
+ return v if isinstance(v, list) else str(v)
+
+ return {
+ k: ConfigValue(make_scalar(v), origin="file", mode="ini")
+ for k, v in ini_config.items()
+ }
+
+ return None
+
+
+def locate_config(
+ invocation_dir: Path,
+ args: Iterable[Path],
+) -> tuple[Path | None, Path | None, ConfigDict, Sequence[str]]:
+ """Search in the list of arguments for a valid ini-file for pytest,
+ and return a tuple of (rootdir, inifile, cfg-dict, ignored-config-files), where
+ ignored-config-files is a list of config basenames found that contain
+ pytest configuration but were ignored."""
+ config_names = [
+ "pytest.toml",
+ ".pytest.toml",
+ "pytest.ini",
+ ".pytest.ini",
+ "pyproject.toml",
+ "tox.ini",
+ "setup.cfg",
+ ]
+ args = [x for x in args if not str(x).startswith("-")]
+ if not args:
+ args = [invocation_dir]
+ found_pyproject_toml: Path | None = None
+ ignored_config_files: list[str] = []
+
+ for arg in args:
+ argpath = absolutepath(arg)
+ for base in (argpath, *argpath.parents):
+ for config_name in config_names:
+ p = base / config_name
+ if p.is_file():
+ if p.name == "pyproject.toml" and found_pyproject_toml is None:
+ found_pyproject_toml = p
+ ini_config = load_config_dict_from_file(p)
+ if ini_config is not None:
+ index = config_names.index(config_name)
+ for remainder in config_names[index + 1 :]:
+ p2 = base / remainder
+ if (
+ p2.is_file()
+ and load_config_dict_from_file(p2) is not None
+ ):
+ ignored_config_files.append(remainder)
+ return base, p, ini_config, ignored_config_files
+ if found_pyproject_toml is not None:
+ return found_pyproject_toml.parent, found_pyproject_toml, {}, []
+ return None, None, {}, []
+
+
+def get_common_ancestor(
+ invocation_dir: Path,
+ paths: Iterable[Path],
+) -> Path:
+ common_ancestor: Path | None = None
+ for path in paths:
+ if not path.exists():
+ continue
+ if common_ancestor is None:
+ common_ancestor = path
+ else:
+ if common_ancestor in path.parents or path == common_ancestor:
+ continue
+ elif path in common_ancestor.parents:
+ common_ancestor = path
+ else:
+ shared = commonpath(path, common_ancestor)
+ if shared is not None:
+ common_ancestor = shared
+ if common_ancestor is None:
+ common_ancestor = invocation_dir
+ elif common_ancestor.is_file():
+ common_ancestor = common_ancestor.parent
+ return common_ancestor
+
+
+def get_dirs_from_args(args: Iterable[str]) -> list[Path]:
+ def is_option(x: str) -> bool:
+ return x.startswith("-")
+
+ def get_file_part_from_node_id(x: str) -> str:
+ return x.split("::", maxsplit=1)[0]
+
+ def get_dir_from_path(path: Path) -> Path:
+ if path.is_dir():
+ return path
+ return path.parent
+
+ # These look like paths but may not exist
+ possible_paths = (
+ absolutepath(get_file_part_from_node_id(arg))
+ for arg in args
+ if not is_option(arg)
+ )
+
+ return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
+
+
+def parse_override_ini(override_ini: Sequence[str] | None) -> ConfigDict:
+ """Parse the -o/--override-ini command line arguments and return the overrides.
+
+ :raises UsageError:
+ If one of the values is malformed.
+ """
+ overrides = {}
+ # override_ini is a list of "ini=value" options.
+ # Always use the last item if multiple values are set for same ini-name,
+ # e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2.
+ for ini_config in override_ini or ():
+ try:
+ key, user_ini_value = ini_config.split("=", 1)
+ except ValueError as e:
+ raise UsageError(
+ f"-o/--override-ini expects option=value style (got: {ini_config!r})."
+ ) from e
+ else:
+ overrides[key] = ConfigValue(user_ini_value, origin="override", mode="ini")
+ return overrides
+
+
+CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
+
+
+def determine_setup(
+ *,
+ inifile: str | None,
+ override_ini: Sequence[str] | None,
+ args: Sequence[str],
+ rootdir_cmd_arg: str | None,
+ invocation_dir: Path,
+) -> tuple[Path, Path | None, ConfigDict, Sequence[str]]:
+ """Determine the rootdir, inifile and ini configuration values from the
+ command line arguments.
+
+ :param inifile:
+ The `--inifile` command line argument, if given.
+ :param override_ini:
+ The -o/--override-ini command line arguments, if given.
+ :param args:
+ The free command line arguments.
+ :param rootdir_cmd_arg:
+ The `--rootdir` command line argument, if given.
+ :param invocation_dir:
+ The working directory when pytest was invoked.
+
+ :raises UsageError:
+ """
+ rootdir = None
+ dirs = get_dirs_from_args(args)
+ ignored_config_files: Sequence[str] = []
+
+ if inifile:
+ inipath_ = absolutepath(inifile)
+ inipath: Path | None = inipath_
+ inicfg = load_config_dict_from_file(inipath_) or {}
+ if rootdir_cmd_arg is None:
+ rootdir = inipath_.parent
+ else:
+ ancestor = get_common_ancestor(invocation_dir, dirs)
+ rootdir, inipath, inicfg, ignored_config_files = locate_config(
+ invocation_dir, [ancestor]
+ )
+ if rootdir is None and rootdir_cmd_arg is None:
+ for possible_rootdir in (ancestor, *ancestor.parents):
+ if (possible_rootdir / "setup.py").is_file():
+ rootdir = possible_rootdir
+ break
+ else:
+ if dirs != [ancestor]:
+ rootdir, inipath, inicfg, _ = locate_config(invocation_dir, dirs)
+ if rootdir is None:
+ rootdir = get_common_ancestor(
+ invocation_dir, [invocation_dir, ancestor]
+ )
+ if is_fs_root(rootdir):
+ rootdir = ancestor
+ if rootdir_cmd_arg:
+ rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
+ if not rootdir.is_dir():
+ raise UsageError(
+ f"Directory '{rootdir}' not found. Check your '--rootdir' option."
+ )
+
+ ini_overrides = parse_override_ini(override_ini)
+ inicfg.update(ini_overrides)
+
+ assert rootdir is not None
+ return rootdir, inipath, inicfg, ignored_config_files
+
+
+def is_fs_root(p: Path) -> bool:
+ r"""
+ Return True if the given path is pointing to the root of the
+ file system ("/" on Unix and "C:\\" on Windows for example).
+ """
+ return os.path.splitdrive(str(p))[1] == os.sep
diff --git a/venv/lib/python3.11/site-packages/_pytest/debugging.py b/venv/lib/python3.11/site-packages/_pytest/debugging.py
new file mode 100644
index 000000000..b256f83c8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/debugging.py
@@ -0,0 +1,404 @@
+# mypy: allow-untyped-defs
+# ruff: noqa: T100
+"""Interactive debugging with PDB, the Python Debugger."""
+
+from __future__ import annotations
+
+import argparse
+from collections.abc import Callable
+from collections.abc import Generator
+import functools
+import importlib
+import sys
+import types
+from typing import Any
+
+from _pytest import outcomes
+from _pytest._code import ExceptionInfo
+from _pytest.capture import CaptureManager
+from _pytest.config import Config
+from _pytest.config import ConftestImportFailure
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.config.argparsing import Parser
+from _pytest.config.exceptions import UsageError
+from _pytest.nodes import Node
+from _pytest.reports import BaseReport
+from _pytest.runner import CallInfo
+
+
+def _validate_usepdb_cls(value: str) -> tuple[str, str]:
+ """Validate syntax of --pdbcls option."""
+ try:
+ modname, classname = value.split(":")
+ except ValueError as e:
+ raise argparse.ArgumentTypeError(
+ f"{value!r} is not in the format 'modname:classname'"
+ ) from e
+ return (modname, classname)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--pdb",
+ dest="usepdb",
+ action="store_true",
+ help="Start the interactive Python debugger on errors or KeyboardInterrupt",
+ )
+ group.addoption(
+ "--pdbcls",
+ dest="usepdb_cls",
+ metavar="modulename:classname",
+ type=_validate_usepdb_cls,
+ help="Specify a custom interactive Python debugger for use with --pdb."
+ "For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
+ )
+ group.addoption(
+ "--trace",
+ dest="trace",
+ action="store_true",
+ help="Immediately break when running each test",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ import pdb
+
+ if config.getvalue("trace"):
+ config.pluginmanager.register(PdbTrace(), "pdbtrace")
+ if config.getvalue("usepdb"):
+ config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
+
+ pytestPDB._saved.append(
+ (pdb.set_trace, pytestPDB._pluginmanager, pytestPDB._config)
+ )
+ pdb.set_trace = pytestPDB.set_trace
+ pytestPDB._pluginmanager = config.pluginmanager
+ pytestPDB._config = config
+
+ # NOTE: not using pytest_unconfigure, since it might get called although
+ # pytest_configure was not (if another plugin raises UsageError).
+ def fin() -> None:
+ (
+ pdb.set_trace,
+ pytestPDB._pluginmanager,
+ pytestPDB._config,
+ ) = pytestPDB._saved.pop()
+
+ config.add_cleanup(fin)
+
+
+class pytestPDB:
+ """Pseudo PDB that defers to the real pdb."""
+
+ _pluginmanager: PytestPluginManager | None = None
+ _config: Config | None = None
+ _saved: list[
+ tuple[Callable[..., None], PytestPluginManager | None, Config | None]
+ ] = []
+ _recursive_debug = 0
+ _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None
+
+ @classmethod
+ def _is_capturing(cls, capman: CaptureManager | None) -> str | bool:
+ if capman:
+ return capman.is_capturing()
+ return False
+
+ @classmethod
+ def _import_pdb_cls(cls, capman: CaptureManager | None):
+ if not cls._config:
+ import pdb
+
+ # Happens when using pytest.set_trace outside of a test.
+ return pdb.Pdb
+
+ usepdb_cls = cls._config.getvalue("usepdb_cls")
+
+ if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls:
+ return cls._wrapped_pdb_cls[1]
+
+ if usepdb_cls:
+ modname, classname = usepdb_cls
+
+ try:
+ mod = importlib.import_module(modname)
+
+ # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp).
+ parts = classname.split(".")
+ pdb_cls = getattr(mod, parts[0])
+ for part in parts[1:]:
+ pdb_cls = getattr(pdb_cls, part)
+ except Exception as exc:
+ value = ":".join((modname, classname))
+ raise UsageError(
+ f"--pdbcls: could not import {value!r}: {exc}"
+ ) from exc
+ else:
+ import pdb
+
+ pdb_cls = pdb.Pdb
+
+ wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman)
+ cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls)
+ return wrapped_cls
+
+ @classmethod
+ def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None):
+ import _pytest.config
+
+ class PytestPdbWrapper(pdb_cls):
+ _pytest_capman = capman
+ _continued = False
+
+ def do_debug(self, arg):
+ cls._recursive_debug += 1
+ ret = super().do_debug(arg)
+ cls._recursive_debug -= 1
+ return ret
+
+ if hasattr(pdb_cls, "do_debug"):
+ do_debug.__doc__ = pdb_cls.do_debug.__doc__
+
+ def do_continue(self, arg):
+ ret = super().do_continue(arg)
+ if cls._recursive_debug == 0:
+ assert cls._config is not None
+ tw = _pytest.config.create_terminal_writer(cls._config)
+ tw.line()
+
+ capman = self._pytest_capman
+ capturing = pytestPDB._is_capturing(capman)
+ if capturing:
+ if capturing == "global":
+ tw.sep(">", "PDB continue (IO-capturing resumed)")
+ else:
+ tw.sep(
+ ">",
+ f"PDB continue (IO-capturing resumed for {capturing})",
+ )
+ assert capman is not None
+ capman.resume()
+ else:
+ tw.sep(">", "PDB continue")
+ assert cls._pluginmanager is not None
+ cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self)
+ self._continued = True
+ return ret
+
+ if hasattr(pdb_cls, "do_continue"):
+ do_continue.__doc__ = pdb_cls.do_continue.__doc__
+
+ do_c = do_cont = do_continue
+
+ def do_quit(self, arg):
+ # Raise Exit outcome when quit command is used in pdb.
+ #
+ # This is a bit of a hack - it would be better if BdbQuit
+ # could be handled, but this would require to wrap the
+ # whole pytest run, and adjust the report etc.
+ ret = super().do_quit(arg)
+
+ if cls._recursive_debug == 0:
+ outcomes.exit("Quitting debugger")
+
+ return ret
+
+ if hasattr(pdb_cls, "do_quit"):
+ do_quit.__doc__ = pdb_cls.do_quit.__doc__
+
+ do_q = do_quit
+ do_exit = do_quit
+
+ def setup(self, f, tb):
+ """Suspend on setup().
+
+ Needed after do_continue resumed, and entering another
+ breakpoint again.
+ """
+ ret = super().setup(f, tb)
+ if not ret and self._continued:
+ # pdb.setup() returns True if the command wants to exit
+ # from the interaction: do not suspend capturing then.
+ if self._pytest_capman:
+ self._pytest_capman.suspend_global_capture(in_=True)
+ return ret
+
+ def get_stack(self, f, t):
+ stack, i = super().get_stack(f, t)
+ if f is None:
+ # Find last non-hidden frame.
+ i = max(0, len(stack) - 1)
+ while i and stack[i][0].f_locals.get("__tracebackhide__", False):
+ i -= 1
+ return stack, i
+
+ return PytestPdbWrapper
+
+ @classmethod
+ def _init_pdb(cls, method, *args, **kwargs):
+ """Initialize PDB debugging, dropping any IO capturing."""
+ import _pytest.config
+
+ if cls._pluginmanager is None:
+ capman: CaptureManager | None = None
+ else:
+ capman = cls._pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend(in_=True)
+
+ if cls._config:
+ tw = _pytest.config.create_terminal_writer(cls._config)
+ tw.line()
+
+ if cls._recursive_debug == 0:
+ # Handle header similar to pdb.set_trace in py37+.
+ header = kwargs.pop("header", None)
+ if header is not None:
+ tw.sep(">", header)
+ else:
+ capturing = cls._is_capturing(capman)
+ if capturing == "global":
+ tw.sep(">", f"PDB {method} (IO-capturing turned off)")
+ elif capturing:
+ tw.sep(
+ ">",
+ f"PDB {method} (IO-capturing turned off for {capturing})",
+ )
+ else:
+ tw.sep(">", f"PDB {method}")
+
+ _pdb = cls._import_pdb_cls(capman)(**kwargs)
+
+ if cls._pluginmanager:
+ cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb)
+ return _pdb
+
+ @classmethod
+ def set_trace(cls, *args, **kwargs) -> None:
+ """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing."""
+ frame = sys._getframe().f_back
+ _pdb = cls._init_pdb("set_trace", *args, **kwargs)
+ _pdb.set_trace(frame)
+
+
+class PdbInvoke:
+ def pytest_exception_interact(
+ self, node: Node, call: CallInfo[Any], report: BaseReport
+ ) -> None:
+ capman = node.config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture(in_=True)
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stdout.write(err)
+ assert call.excinfo is not None
+ _enter_pdb(node, call.excinfo, report)
+
+ def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
+ exc_or_tb = _postmortem_exc_or_tb(excinfo)
+ post_mortem(exc_or_tb)
+
+
+class PdbTrace:
+ @hookimpl(wrapper=True)
+ def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
+ wrap_pytest_function_for_tracing(pyfuncitem)
+ return (yield)
+
+
+def wrap_pytest_function_for_tracing(pyfuncitem) -> None:
+ """Change the Python function object of the given Function item by a
+ wrapper which actually enters pdb before calling the python function
+ itself, effectively leaving the user in the pdb prompt in the first
+ statement of the function."""
+ _pdb = pytestPDB._init_pdb("runcall")
+ testfunction = pyfuncitem.obj
+
+ # we can't just return `partial(pdb.runcall, testfunction)` because (on
+ # python < 3.7.4) runcall's first param is `func`, which means we'd get
+ # an exception if one of the kwargs to testfunction was called `func`.
+ @functools.wraps(testfunction)
+ def wrapper(*args, **kwargs) -> None:
+ func = functools.partial(testfunction, *args, **kwargs)
+ _pdb.runcall(func)
+
+ pyfuncitem.obj = wrapper
+
+
+def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None:
+ """Wrap the given pytestfunct item for tracing support if --trace was given in
+ the command line."""
+ if pyfuncitem.config.getvalue("trace"):
+ wrap_pytest_function_for_tracing(pyfuncitem)
+
+
+def _enter_pdb(
+ node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport
+) -> BaseReport:
+ # XXX we reuse the TerminalReporter's terminalwriter
+ # because this seems to avoid some encoding related troubles
+ # for not completely clear reasons.
+ tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
+ tw.line()
+
+ showcapture = node.config.option.showcapture
+
+ for sectionname, content in (
+ ("stdout", rep.capstdout),
+ ("stderr", rep.capstderr),
+ ("log", rep.caplog),
+ ):
+ if showcapture in (sectionname, "all") and content:
+ tw.sep(">", "captured " + sectionname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ tw.line(content)
+
+ tw.sep(">", "traceback")
+ rep.toterminal(tw)
+ tw.sep(">", "entering PDB")
+ tb_or_exc = _postmortem_exc_or_tb(excinfo)
+ rep._pdbshown = True # type: ignore[attr-defined]
+ post_mortem(tb_or_exc)
+ return rep
+
+
+def _postmortem_exc_or_tb(
+ excinfo: ExceptionInfo[BaseException],
+) -> types.TracebackType | BaseException:
+ from doctest import UnexpectedException
+
+ get_exc = sys.version_info >= (3, 13)
+ if isinstance(excinfo.value, UnexpectedException):
+ # A doctest.UnexpectedException is not useful for post_mortem.
+ # Use the underlying exception instead:
+ underlying_exc = excinfo.value
+ if get_exc:
+ return underlying_exc.exc_info[1]
+
+ return underlying_exc.exc_info[2]
+ elif isinstance(excinfo.value, ConftestImportFailure):
+ # A config.ConftestImportFailure is not useful for post_mortem.
+ # Use the underlying exception instead:
+ cause = excinfo.value.cause
+ if get_exc:
+ return cause
+
+ assert cause.__traceback__ is not None
+ return cause.__traceback__
+ else:
+ assert excinfo._excinfo is not None
+ if get_exc:
+ return excinfo._excinfo[1]
+
+ return excinfo._excinfo[2]
+
+
+def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None:
+ p = pytestPDB._init_pdb("post_mortem")
+ p.reset()
+ p.interaction(None, tb_or_exc)
+ if p.quitting:
+ outcomes.exit("Quitting debugger")
diff --git a/venv/lib/python3.11/site-packages/_pytest/deprecated.py b/venv/lib/python3.11/site-packages/_pytest/deprecated.py
new file mode 100644
index 000000000..95e75e60f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/deprecated.py
@@ -0,0 +1,139 @@
+"""Deprecation messages and bits of code used elsewhere in the codebase that
+is planned to be removed in the next pytest release.
+
+Keeping it in a central location makes it easy to track what is deprecated and should
+be removed when the time comes.
+
+All constants defined in this module should be either instances of
+:class:`PytestWarning`, or :class:`UnformattedWarning`
+in case of warnings which need to format their messages.
+"""
+
+from __future__ import annotations
+
+from warnings import warn
+
+from _pytest.warning_types import PytestDeprecationWarning
+from _pytest.warning_types import PytestRemovedIn10Warning
+from _pytest.warning_types import UnformattedWarning
+
+
+# set of plugins which have been integrated into the core; we use this list to ignore
+# them during registration to avoid conflicts
+DEPRECATED_EXTERNAL_PLUGINS = {
+ "pytest_catchlog",
+ "pytest_capturelog",
+ "pytest_faulthandler",
+ "pytest_subtests",
+}
+
+
+# This could have been removed pytest 8, but it's harmless and common, so no rush to remove.
+YIELD_FIXTURE = PytestDeprecationWarning(
+ "@pytest.yield_fixture is deprecated.\n"
+ "Use @pytest.fixture instead; they are the same."
+)
+
+CLASS_FIXTURE_INSTANCE_METHOD = PytestRemovedIn10Warning(
+ "Class-scoped fixture defined as instance method is deprecated.\n"
+ "Instance attributes set in this fixture will NOT be visible to test methods,\n"
+ "as each test gets a new instance while the fixture runs only once per class.\n"
+ "Use @classmethod decorator and set attributes on cls instead.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html#class-scoped-fixture-as-instance-method"
+)
+
+# This deprecation is never really meant to be removed.
+PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
+
+
+HOOK_LEGACY_MARKING = UnformattedWarning(
+ PytestRemovedIn10Warning,
+ "The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
+ "Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
+ " to configure the hooks.\n"
+ " See https://docs.pytest.org/en/latest/deprecations.html"
+ "#configuring-hook-specs-impls-using-markers",
+)
+
+MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning(
+ "monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n"
+ "Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n"
+ "Please use native namespace packages (PEP 420) instead.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html#monkeypatch-fixup-namespace-packages"
+)
+
+PARAMETRIZE_NON_COLLECTION_ITERABLE = UnformattedWarning(
+ PytestRemovedIn10Warning,
+ "Passing a non-Collection iterable to parametrize is deprecated.\n"
+ "Test: {nodeid}, argvalues type: {type_name}\n"
+ "Please convert to a list or tuple.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html#parametrize-iterators",
+)
+
+CONSOLE_MAIN = PytestRemovedIn10Warning(
+ "pytest.console_main() is deprecated and will be removed in pytest 10.\n"
+ "It was never intended for programmatic use; use pytest.main() instead.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html#console-main"
+)
+
+CONFIG_INICFG = PytestRemovedIn10Warning(
+ "config.inicfg is deprecated, use config.getini() to access configuration values instead.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html#config-inicfg"
+)
+
+FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN = UnformattedWarning(
+ PytestRemovedIn10Warning,
+ 'Calling request.getfixturevalue("{argname}") during teardown is deprecated.\n'
+ "Please request the fixture before teardown begins, either by declaring it in the fixture signature "
+ "or by calling request.getfixturevalue() before the fixture yields.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html#dynamic-fixture-request-during-teardown",
+)
+
+PASTEBIN = PytestRemovedIn10Warning(
+ "The --pastebin option is deprecated. "
+ "The functionality is now available in an external plugin package, pytest-pastebin.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html#the-pastebin-option"
+)
+
+# You want to make some `__init__` or function "private".
+#
+# def my_private_function(some, args):
+# ...
+#
+# Do this:
+#
+# def my_private_function(some, args, *, _ispytest: bool = False):
+# check_ispytest(_ispytest)
+# ...
+#
+# Change all internal/allowed calls to
+#
+# my_private_function(some, args, _ispytest=True)
+#
+# All other calls will get the default _ispytest=False and trigger
+# the warning (possibly error in the future).
+
+
+FIXTURE_BASEID_DEPRECATED = PytestRemovedIn10Warning(
+ "Passing baseid to FixtureDef is deprecated. Pass node instead for fixture scoping."
+)
+
+FIXTURE_NODEID_DEPRECATED = PytestRemovedIn10Warning(
+ "Passing nodeid to _register_fixture is deprecated. "
+ "Pass node instead for fixture scoping."
+)
+
+FIXTUREDEF_HAS_LOCATION_DEPRECATED = PytestRemovedIn10Warning(
+ "FixtureDef.has_location is deprecated and will be removed in pytest 10. "
+ "See https://docs.pytest.org/en/stable/deprecations.html#fixturedef-has-location-deprecated"
+)
+
+PARSEFACTORIES_NODEID_DEPRECATED = PytestRemovedIn10Warning(
+ "Passing nodeid string to parsefactories is deprecated. "
+ "Use parsefactories(holder=obj, node=node) instead."
+)
+
+
+def check_ispytest(ispytest: bool) -> None:
+ if not ispytest:
+ warn(PRIVATE, stacklevel=3)
diff --git a/venv/lib/python3.11/site-packages/_pytest/doctest.py b/venv/lib/python3.11/site-packages/_pytest/doctest.py
new file mode 100644
index 000000000..b1f365109
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/doctest.py
@@ -0,0 +1,735 @@
+# mypy: allow-untyped-defs
+"""Discover and run doctests in modules and test files."""
+
+from __future__ import annotations
+
+import bdb
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Sequence
+from contextlib import contextmanager
+import functools
+import inspect
+import os
+from pathlib import Path
+import platform
+import re
+import sys
+import traceback
+import types
+from typing import Any
+from typing import TYPE_CHECKING
+import warnings
+
+from _pytest import outcomes
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ReprFileLocation
+from _pytest._code.code import TerminalRepr
+from _pytest._io import TerminalWriter
+from _pytest.compat import safe_getattr
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import fixture
+from _pytest.fixtures import TopRequest
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import OutcomeException
+from _pytest.outcomes import skip
+from _pytest.pathlib import fnmatch_ex
+from _pytest.python import Module
+from _pytest.python_api import approx
+from _pytest.warning_types import PytestWarning
+
+
+if TYPE_CHECKING:
+ import doctest
+
+ from typing_extensions import Self
+
+DOCTEST_REPORT_CHOICE_NONE = "none"
+DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
+DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
+DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
+DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
+
+DOCTEST_REPORT_CHOICES = (
+ DOCTEST_REPORT_CHOICE_NONE,
+ DOCTEST_REPORT_CHOICE_CDIFF,
+ DOCTEST_REPORT_CHOICE_NDIFF,
+ DOCTEST_REPORT_CHOICE_UDIFF,
+ DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
+)
+
+# Lazy definition of runner class
+RUNNER_CLASS = None
+# Lazy definition of output checker class
+CHECKER_CLASS: type[doctest.OutputChecker] | None = None
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "doctest_optionflags",
+ "Option flags for doctests",
+ type="args",
+ default=["ELLIPSIS"],
+ )
+ parser.addini(
+ "doctest_encoding", "Encoding used for doctest files", default="utf-8"
+ )
+ group = parser.getgroup("collect")
+ group.addoption(
+ "--doctest-modules",
+ action="store_true",
+ default=False,
+ help="Run doctests in all .py modules",
+ dest="doctestmodules",
+ )
+ group.addoption(
+ "--doctest-report",
+ type=str.lower,
+ default="udiff",
+ help="Choose another output format for diffs on doctest failure",
+ choices=DOCTEST_REPORT_CHOICES,
+ dest="doctestreport",
+ )
+ group.addoption(
+ "--doctest-glob",
+ action="append",
+ default=[],
+ metavar="pat",
+ help="Doctests file matching pattern, default: test*.txt",
+ dest="doctestglob",
+ )
+ group.addoption(
+ "--doctest-ignore-import-errors",
+ action="store_true",
+ default=False,
+ help="Ignore doctest collection errors",
+ dest="doctest_ignore_import_errors",
+ )
+ group.addoption(
+ "--doctest-continue-on-failure",
+ action="store_true",
+ default=False,
+ help="For a given doctest, continue to run after the first failure",
+ dest="doctest_continue_on_failure",
+ )
+
+
+def pytest_unconfigure() -> None:
+ global RUNNER_CLASS
+
+ RUNNER_CLASS = None
+
+
+def pytest_collect_file(
+ file_path: Path,
+ parent: Collector,
+) -> DoctestModule | DoctestTextfile | None:
+ config = parent.config
+ if file_path.suffix == ".py":
+ if config.option.doctestmodules and not any(
+ (_is_setup_py(file_path), _is_main_py(file_path))
+ ):
+ return DoctestModule.from_parent(parent, path=file_path)
+ elif _is_doctest(config, file_path, parent):
+ return DoctestTextfile.from_parent(parent, path=file_path)
+ return None
+
+
+def _is_setup_py(path: Path) -> bool:
+ if path.name != "setup.py":
+ return False
+ contents = path.read_bytes()
+ return b"setuptools" in contents or b"distutils" in contents
+
+
+def _is_doctest(config: Config, path: Path, parent: Collector) -> bool:
+ if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
+ return True
+ globs = config.getoption("doctestglob") or ["test*.txt"]
+ return any(fnmatch_ex(glob, path) for glob in globs)
+
+
+def _is_main_py(path: Path) -> bool:
+ return path.name == "__main__.py"
+
+
+class ReprFailDoctest(TerminalRepr):
+ def __init__(
+ self, reprlocation_lines: Sequence[tuple[ReprFileLocation, Sequence[str]]]
+ ) -> None:
+ self.reprlocation_lines = reprlocation_lines
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ for reprlocation, lines in self.reprlocation_lines:
+ for line in lines:
+ tw.line(line)
+ reprlocation.toterminal(tw)
+
+
+class MultipleDoctestFailures(Exception):
+ def __init__(self, failures: Sequence[doctest.DocTestFailure]) -> None:
+ super().__init__()
+ self.failures = failures
+
+
+def _init_runner_class() -> type[doctest.DocTestRunner]:
+ import doctest
+
+ class PytestDoctestRunner(doctest.DebugRunner):
+ """Runner to collect failures.
+
+ Note that the out variable in this case is a list instead of a
+ stdout-like object.
+ """
+
+ def __init__(
+ self,
+ checker: doctest.OutputChecker | None = None,
+ verbose: bool | None = None,
+ optionflags: int = 0,
+ continue_on_failure: bool = True,
+ ) -> None:
+ super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
+ self.continue_on_failure = continue_on_failure
+
+ def report_failure(
+ self,
+ out,
+ test: doctest.DocTest,
+ example: doctest.Example,
+ got: str,
+ ) -> None:
+ failure = doctest.DocTestFailure(test, example, got)
+ if self.continue_on_failure:
+ out.append(failure)
+ else:
+ raise failure
+
+ def report_unexpected_exception(
+ self,
+ out,
+ test: doctest.DocTest,
+ example: doctest.Example,
+ exc_info: tuple[type[BaseException], BaseException, types.TracebackType],
+ ) -> None:
+ if isinstance(exc_info[1], OutcomeException):
+ raise exc_info[1]
+ if isinstance(exc_info[1], bdb.BdbQuit):
+ outcomes.exit("Quitting debugger")
+ failure = doctest.UnexpectedException(test, example, exc_info)
+ if self.continue_on_failure:
+ out.append(failure)
+ else:
+ raise failure
+
+ return PytestDoctestRunner
+
+
+def _get_runner(
+ checker: doctest.OutputChecker | None = None,
+ verbose: bool | None = None,
+ optionflags: int = 0,
+ continue_on_failure: bool = True,
+) -> doctest.DocTestRunner:
+ # We need this in order to do a lazy import on doctest
+ global RUNNER_CLASS
+ if RUNNER_CLASS is None:
+ RUNNER_CLASS = _init_runner_class()
+ # Type ignored because the continue_on_failure argument is only defined on
+ # PytestDoctestRunner, which is lazily defined so can't be used as a type.
+ return RUNNER_CLASS( # type: ignore
+ checker=checker,
+ verbose=verbose,
+ optionflags=optionflags,
+ continue_on_failure=continue_on_failure,
+ )
+
+
+class DoctestItem(Item):
+ def __init__(
+ self,
+ name: str,
+ parent: DoctestTextfile | DoctestModule,
+ runner: doctest.DocTestRunner,
+ dtest: doctest.DocTest,
+ ) -> None:
+ super().__init__(name, parent)
+ self.runner = runner
+ self.dtest = dtest
+
+ # Stuff needed for fixture support.
+ self.obj = None
+ fm = self.session._fixturemanager
+ fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
+ self._fixtureinfo = fixtureinfo
+ self.fixturenames = fixtureinfo.names_closure
+ self._initrequest()
+
+ @classmethod
+ def from_parent( # type: ignore[override]
+ cls,
+ parent: DoctestTextfile | DoctestModule,
+ *,
+ name: str,
+ runner: doctest.DocTestRunner,
+ dtest: doctest.DocTest,
+ ) -> Self:
+ # incompatible signature due to imposed limits on subclass
+ """The public named constructor."""
+ return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
+
+ def _initrequest(self) -> None:
+ self.funcargs: dict[str, object] = {}
+ self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
+
+ def setup(self) -> None:
+ self._request._fillfixtures()
+ globs = dict(getfixture=self._request.getfixturevalue)
+ for name, value in self._request.getfixturevalue("doctest_namespace").items():
+ globs[name] = value
+ self.dtest.globs.update(globs)
+
+ def runtest(self) -> None:
+ _check_all_skipped(self.dtest)
+ self._disable_output_capturing_for_darwin()
+ failures: list[doctest.DocTestFailure] = []
+ # Type ignored because we change the type of `out` from what
+ # doctest expects.
+ self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
+ if failures:
+ raise MultipleDoctestFailures(failures)
+
+ def _disable_output_capturing_for_darwin(self) -> None:
+ """Disable output capturing. Otherwise, stdout is lost to doctest (#985)."""
+ if platform.system() != "Darwin":
+ return
+ capman = self.config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture(in_=True)
+ out, err = capman.read_global_capture()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+ # TODO: Type ignored -- breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ ) -> str | TerminalRepr:
+ import doctest
+
+ failures: (
+ Sequence[doctest.DocTestFailure | doctest.UnexpectedException] | None
+ ) = None
+ if isinstance(
+ excinfo.value, doctest.DocTestFailure | doctest.UnexpectedException
+ ):
+ failures = [excinfo.value]
+ elif isinstance(excinfo.value, MultipleDoctestFailures):
+ failures = excinfo.value.failures
+
+ if failures is None:
+ return super().repr_failure(excinfo)
+
+ reprlocation_lines = []
+ for failure in failures:
+ example = failure.example
+ test = failure.test
+ filename = test.filename
+ if test.lineno is None:
+ lineno = None
+ else:
+ lineno = test.lineno + example.lineno + 1
+ message = type(failure).__name__
+ # TODO: ReprFileLocation doesn't expect a None lineno.
+ reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
+ checker = _get_checker()
+ report_choice = _get_report_choice(self.config.getoption("doctestreport"))
+ if lineno is not None:
+ assert failure.test.docstring is not None
+ lines = failure.test.docstring.splitlines(False)
+ # add line numbers to the left of the error message
+ assert test.lineno is not None
+ lines = [
+ f"{i + test.lineno + 1:03d} {x}" for (i, x) in enumerate(lines)
+ ]
+ # trim docstring error lines to 10
+ lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
+ else:
+ lines = [
+ "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
+ ]
+ indent = ">>>"
+ for line in example.source.splitlines():
+ lines.append(f"??? {indent} {line}")
+ indent = "..."
+ if isinstance(failure, doctest.DocTestFailure):
+ lines += checker.output_difference(
+ example, failure.got, report_choice
+ ).split("\n")
+ else:
+ inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
+ lines += [f"UNEXPECTED EXCEPTION: {inner_excinfo.value!r}"]
+ lines += [
+ x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
+ ]
+ reprlocation_lines.append((reprlocation, lines))
+ return ReprFailDoctest(reprlocation_lines)
+
+ def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
+ return self.path, self.dtest.lineno, f"[doctest] {self.name}"
+
+
+def _get_flag_lookup() -> dict[str, int]:
+ import doctest
+
+ return dict(
+ DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
+ DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
+ NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
+ ELLIPSIS=doctest.ELLIPSIS,
+ IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
+ COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
+ ALLOW_UNICODE=_get_allow_unicode_flag(),
+ ALLOW_BYTES=_get_allow_bytes_flag(),
+ NUMBER=_get_number_flag(),
+ )
+
+
+def get_optionflags(config: Config) -> int:
+ optionflags_str = config.getini("doctest_optionflags")
+ flag_lookup_table = _get_flag_lookup()
+ flag_acc = 0
+ for flag in optionflags_str:
+ flag_acc |= flag_lookup_table[flag]
+ return flag_acc
+
+
+def _get_continue_on_failure(config: Config) -> bool:
+ continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
+ if continue_on_failure:
+ # We need to turn off this if we use pdb since we should stop at
+ # the first failure.
+ if config.getvalue("usepdb"):
+ continue_on_failure = False
+ return continue_on_failure
+
+
+class DoctestTextfile(Module):
+ obj = None
+
+ def collect(self) -> Iterable[DoctestItem]:
+ import doctest
+
+ # Inspired by doctest.testfile; ideally we would use it directly,
+ # but it doesn't support passing a custom checker.
+ encoding = self.config.getini("doctest_encoding")
+ text = self.path.read_text(encoding)
+ filename = str(self.path)
+ name = self.path.name
+ globs = {"__name__": "__main__"}
+
+ optionflags = get_optionflags(self.config)
+
+ runner = _get_runner(
+ verbose=False,
+ optionflags=optionflags,
+ checker=_get_checker(),
+ continue_on_failure=_get_continue_on_failure(self.config),
+ )
+
+ parser = doctest.DocTestParser()
+ test = parser.get_doctest(text, globs, name, filename, 0)
+ if test.examples:
+ yield DoctestItem.from_parent(
+ self, name=test.name, runner=runner, dtest=test
+ )
+
+
+def _check_all_skipped(test: doctest.DocTest) -> None:
+ """Raise pytest.skip() if all examples in the given DocTest have the SKIP
+ option set."""
+ import doctest
+
+ all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
+ if all_skipped:
+ skip("all tests skipped by +SKIP option")
+
+
+def _is_mocked(obj: object) -> bool:
+ """Return if an object is possibly a mock object by checking the
+ existence of a highly improbable attribute."""
+ return (
+ safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
+ is not None
+ )
+
+
+@contextmanager
+def _patch_unwrap_mock_aware() -> Generator[None]:
+ """Context manager which replaces ``inspect.unwrap`` with a version
+ that's aware of mock objects and doesn't recurse into them."""
+ real_unwrap = inspect.unwrap
+
+ def _mock_aware_unwrap(
+ func: Callable[..., Any], *, stop: Callable[[Any], Any] | None = None
+ ) -> Any:
+ try:
+ if stop is None or stop is _is_mocked:
+ return real_unwrap(func, stop=_is_mocked)
+ _stop = stop
+ return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
+ except Exception as e:
+ warnings.warn(
+ f"Got {e!r} when unwrapping {func!r}. This is usually caused "
+ "by a violation of Python's object protocol; see e.g. "
+ "https://github.com/pytest-dev/pytest/issues/5080",
+ PytestWarning,
+ )
+ raise
+
+ inspect.unwrap = _mock_aware_unwrap
+ try:
+ yield
+ finally:
+ inspect.unwrap = real_unwrap
+
+
+class DoctestModule(Module):
+ def collect(self) -> Iterable[DoctestItem]:
+ import doctest
+
+ class MockAwareDocTestFinder(doctest.DocTestFinder):
+ py_ver_info_minor = sys.version_info[:2]
+ is_find_lineno_broken = (
+ py_ver_info_minor < (3, 11)
+ or (py_ver_info_minor == (3, 11) and sys.version_info.micro < 9)
+ or (py_ver_info_minor == (3, 12) and sys.version_info.micro < 3)
+ )
+ if is_find_lineno_broken:
+
+ def _find_lineno(self, obj, source_lines):
+ """On older Pythons, doctest code does not take into account
+ `@property`. https://github.com/python/cpython/issues/61648
+
+ Moreover, wrapped Doctests need to be unwrapped so the correct
+ line number is returned. #8796
+ """
+ if isinstance(obj, property):
+ obj = getattr(obj, "fget", obj)
+
+ if hasattr(obj, "__wrapped__"):
+ # Get the main obj in case of it being wrapped
+ obj = inspect.unwrap(obj)
+
+ # Type ignored because this is a private function.
+ return super()._find_lineno( # type:ignore[misc]
+ obj,
+ source_lines,
+ )
+
+ if sys.version_info < (3, 13):
+
+ def _from_module(self, module, object):
+ """`cached_property` objects are never considered a part
+ of the 'current module'. As such they are skipped by doctest.
+ Here we override `_from_module` to check the underlying
+ function instead. https://github.com/python/cpython/issues/107995
+ """
+ if isinstance(object, functools.cached_property):
+ object = object.func
+
+ # Type ignored because this is a private function.
+ return super()._from_module(module, object) # type: ignore[misc]
+
+ try:
+ module = self.obj
+ except Collector.CollectError:
+ if self.config.getvalue("doctest_ignore_import_errors"):
+ skip(f"unable to import module {self.path!r}")
+ else:
+ raise
+
+ # doctests supports fixtures via `getfixture` and autouse.
+ self.session._fixturemanager.parsefactories(self)
+
+ # Uses internal doctest module parsing mechanism.
+ finder = MockAwareDocTestFinder()
+ optionflags = get_optionflags(self.config)
+ runner = _get_runner(
+ verbose=False,
+ optionflags=optionflags,
+ checker=_get_checker(),
+ continue_on_failure=_get_continue_on_failure(self.config),
+ )
+
+ for test in finder.find(module, module.__name__):
+ if test.examples: # skip empty doctests
+ yield DoctestItem.from_parent(
+ self, name=test.name, runner=runner, dtest=test
+ )
+
+
+def _init_checker_class() -> type[doctest.OutputChecker]:
+ import doctest
+
+ class LiteralsOutputChecker(doctest.OutputChecker):
+ # Based on doctest_nose_plugin.py from the nltk project
+ # (https://github.com/nltk/nltk) and on the "numtest" doctest extension
+ # by Sebastien Boisgerault (https://github.com/boisgera/numtest).
+
+ _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
+ _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
+ _number_re = re.compile(
+ r"""
+ (?P
+ (?P
+ (?P [+-]?\d*)\.(?P\d+)
+ |
+ (?P [+-]?\d+)\.
+ )
+ (?:
+ [Ee]
+ (?P [+-]?\d+)
+ )?
+ |
+ (?P [+-]?\d+)
+ (?:
+ [Ee]
+ (?P [+-]?\d+)
+ )
+ )
+ """,
+ re.VERBOSE,
+ )
+
+ def check_output(self, want: str, got: str, optionflags: int) -> bool:
+ if super().check_output(want, got, optionflags):
+ return True
+
+ allow_unicode = optionflags & _get_allow_unicode_flag()
+ allow_bytes = optionflags & _get_allow_bytes_flag()
+ allow_number = optionflags & _get_number_flag()
+
+ if not allow_unicode and not allow_bytes and not allow_number:
+ return False
+
+ def remove_prefixes(regex: re.Pattern[str], txt: str) -> str:
+ return re.sub(regex, r"\1\2", txt)
+
+ if allow_unicode:
+ want = remove_prefixes(self._unicode_literal_re, want)
+ got = remove_prefixes(self._unicode_literal_re, got)
+
+ if allow_bytes:
+ want = remove_prefixes(self._bytes_literal_re, want)
+ got = remove_prefixes(self._bytes_literal_re, got)
+
+ if allow_number:
+ got = self._remove_unwanted_precision(want, got)
+
+ return super().check_output(want, got, optionflags)
+
+ def _remove_unwanted_precision(self, want: str, got: str) -> str:
+ wants = list(self._number_re.finditer(want))
+ gots = list(self._number_re.finditer(got))
+ if len(wants) != len(gots):
+ return got
+ offset = 0
+ for w, g in zip(wants, gots, strict=True):
+ fraction: str | None = w.group("fraction")
+ exponent: str | None = w.group("exponent1")
+ if exponent is None:
+ exponent = w.group("exponent2")
+ precision = 0 if fraction is None else len(fraction)
+ if exponent is not None:
+ precision -= int(exponent)
+ if float(w.group()) == approx(float(g.group()), abs=10**-precision):
+ # They're close enough. Replace the text we actually
+ # got with the text we want, so that it will match when we
+ # check the string literally.
+ got = (
+ got[: g.start() + offset] + w.group() + got[g.end() + offset :]
+ )
+ offset += w.end() - w.start() - (g.end() - g.start())
+ return got
+
+ return LiteralsOutputChecker
+
+
+def _get_checker() -> doctest.OutputChecker:
+ """Return a doctest.OutputChecker subclass that supports some
+ additional options:
+
+ * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
+ prefixes (respectively) in string literals. Useful when the same
+ doctest should run in Python 2 and Python 3.
+
+ * NUMBER to ignore floating-point differences smaller than the
+ precision of the literal number in the doctest.
+
+ An inner class is used to avoid importing "doctest" at the module
+ level.
+ """
+ global CHECKER_CLASS
+ if CHECKER_CLASS is None:
+ CHECKER_CLASS = _init_checker_class()
+ return CHECKER_CLASS()
+
+
+def _get_allow_unicode_flag() -> int:
+ """Register and return the ALLOW_UNICODE flag."""
+ import doctest
+
+ return doctest.register_optionflag("ALLOW_UNICODE")
+
+
+def _get_allow_bytes_flag() -> int:
+ """Register and return the ALLOW_BYTES flag."""
+ import doctest
+
+ return doctest.register_optionflag("ALLOW_BYTES")
+
+
+def _get_number_flag() -> int:
+ """Register and return the NUMBER flag."""
+ import doctest
+
+ return doctest.register_optionflag("NUMBER")
+
+
+def _get_report_choice(key: str) -> int:
+ """Return the actual `doctest` module flag value.
+
+ We want to do it as late as possible to avoid importing `doctest` and all
+ its dependencies when parsing options, as it adds overhead and breaks tests.
+ """
+ import doctest
+
+ return {
+ DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
+ DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
+ DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
+ DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
+ DOCTEST_REPORT_CHOICE_NONE: 0,
+ }[key]
+
+
+@fixture(scope="session")
+def doctest_namespace() -> dict[str, Any]:
+ """Fixture that returns a :py:class:`dict` that will be injected into the
+ namespace of doctests.
+
+ Usually this fixture is used in conjunction with another ``autouse`` fixture:
+
+ .. code-block:: python
+
+ @pytest.fixture(autouse=True)
+ def add_np(doctest_namespace):
+ doctest_namespace["np"] = numpy
+
+ For more details: :ref:`doctest_namespace`.
+ """
+ return dict()
diff --git a/venv/lib/python3.11/site-packages/_pytest/faulthandler.py b/venv/lib/python3.11/site-packages/_pytest/faulthandler.py
new file mode 100644
index 000000000..080cf5838
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/faulthandler.py
@@ -0,0 +1,119 @@
+from __future__ import annotations
+
+from collections.abc import Generator
+import os
+import sys
+
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+from _pytest.stash import StashKey
+import pytest
+
+
+fault_handler_original_stderr_fd_key = StashKey[int]()
+fault_handler_stderr_fd_key = StashKey[int]()
+
+
+def pytest_addoption(parser: Parser) -> None:
+ help_timeout = (
+ "Dump the traceback of all threads if a test takes "
+ "more than TIMEOUT seconds to finish"
+ )
+ help_exit_on_timeout = (
+ "Exit the test process if a test takes more than "
+ "faulthandler_timeout seconds to finish"
+ )
+ parser.addini("faulthandler_timeout", help_timeout, default=0.0)
+ parser.addini(
+ "faulthandler_exit_on_timeout", help_exit_on_timeout, type="bool", default=False
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ import faulthandler
+
+ # at teardown we want to restore the original faulthandler fileno
+ # but faulthandler has no api to return the original fileno
+ # so here we stash the stderr fileno to be used at teardown
+ # sys.stderr and sys.__stderr__ may be closed or patched during the session
+ # so we can't rely on their values being good at that point (#11572).
+ stderr_fileno = get_stderr_fileno()
+ if faulthandler.is_enabled():
+ config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
+ config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
+ faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
+
+
+def pytest_unconfigure(config: Config) -> None:
+ import faulthandler
+
+ faulthandler.disable()
+ # Close the dup file installed during pytest_configure.
+ if fault_handler_stderr_fd_key in config.stash:
+ os.close(config.stash[fault_handler_stderr_fd_key])
+ del config.stash[fault_handler_stderr_fd_key]
+ # Re-enable the faulthandler if it was originally enabled.
+ if fault_handler_original_stderr_fd_key in config.stash:
+ faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
+ del config.stash[fault_handler_original_stderr_fd_key]
+
+
+def get_stderr_fileno() -> int:
+ try:
+ fileno = sys.stderr.fileno()
+ # The Twisted Logger will return an invalid file descriptor since it is not backed
+ # by an FD. So, let's also forward this to the same code path as with pytest-xdist.
+ if fileno == -1:
+ raise AttributeError()
+ return fileno
+ except (AttributeError, ValueError):
+ # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
+ # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
+ # This is potentially dangerous, but the best we can do.
+ assert sys.__stderr__ is not None
+ return sys.__stderr__.fileno()
+
+
+def get_timeout_config_value(config: Config) -> float:
+ return float(config.getini("faulthandler_timeout") or 0.0)
+
+
+def get_exit_on_timeout_config_value(config: Config) -> bool:
+ exit_on_timeout = config.getini("faulthandler_exit_on_timeout")
+ assert isinstance(exit_on_timeout, bool)
+ return exit_on_timeout
+
+
+@pytest.hookimpl(wrapper=True, trylast=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
+ timeout = get_timeout_config_value(item.config)
+ exit_on_timeout = get_exit_on_timeout_config_value(item.config)
+ if timeout > 0:
+ import faulthandler
+
+ stderr = item.config.stash[fault_handler_stderr_fd_key]
+ faulthandler.dump_traceback_later(timeout, file=stderr, exit=exit_on_timeout)
+ try:
+ return (yield)
+ finally:
+ faulthandler.cancel_dump_traceback_later()
+ else:
+ return (yield)
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_enter_pdb() -> None:
+ """Cancel any traceback dumping due to timeout before entering pdb."""
+ import faulthandler
+
+ faulthandler.cancel_dump_traceback_later()
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_exception_interact() -> None:
+ """Cancel any traceback dumping due to an interactive exception being
+ raised."""
+ import faulthandler
+
+ faulthandler.cancel_dump_traceback_later()
diff --git a/venv/lib/python3.11/site-packages/_pytest/fixtures.py b/venv/lib/python3.11/site-packages/_pytest/fixtures.py
new file mode 100644
index 000000000..f4ca2eac4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/fixtures.py
@@ -0,0 +1,2393 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+import abc
+from collections import defaultdict
+from collections import deque
+from collections import OrderedDict
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Mapping
+from collections.abc import MutableMapping
+from collections.abc import Sequence
+from collections.abc import Set as AbstractSet
+import dataclasses
+import functools
+import inspect
+import os
+from pathlib import Path
+import sys
+import types
+from typing import Any
+from typing import cast
+from typing import Final
+from typing import final
+from typing import Generic
+from typing import Literal
+from typing import NoReturn
+from typing import overload
+from typing import TYPE_CHECKING
+from typing import TypeVar
+import warnings
+
+import _pytest
+from _pytest import nodes
+from _pytest._code import getfslineno
+from _pytest._code import Source
+from _pytest._code.code import ExceptionInfoFormatter
+from _pytest._code.code import TerminalRepr
+from _pytest._io import TerminalWriter
+from _pytest.compat import assert_never
+from _pytest.compat import deprecated
+from _pytest.compat import get_real_func
+from _pytest.compat import getfuncargnames
+from _pytest.compat import getimfunc
+from _pytest.compat import getlocation
+from _pytest.compat import NOTSET
+from _pytest.compat import NotSetType
+from _pytest.compat import safe_getattr
+from _pytest.compat import safe_isclass
+from _pytest.compat import signature
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.deprecated import CLASS_FIXTURE_INSTANCE_METHOD
+from _pytest.deprecated import FIXTURE_BASEID_DEPRECATED
+from _pytest.deprecated import FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN
+from _pytest.deprecated import FIXTURE_NODEID_DEPRECATED
+from _pytest.deprecated import FIXTUREDEF_HAS_LOCATION_DEPRECATED
+from _pytest.deprecated import PARSEFACTORIES_NODEID_DEPRECATED
+from _pytest.deprecated import YIELD_FIXTURE
+from _pytest.main import Session
+from _pytest.mark import Mark
+from _pytest.mark import ParameterSet
+from _pytest.mark.structures import MarkDecorator
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.outcomes import TEST_OUTCOME
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.scope import HIGH_SCOPES
+from _pytest.scope import Scope
+from _pytest.scope import ScopeName
+from _pytest.warning_types import PytestWarning
+
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+
+if TYPE_CHECKING:
+ from _pytest.python import CallSpec2
+ from _pytest.python import Function
+ from _pytest.python import Metafunc
+ from _pytest.reports import CollectReport
+
+
+# The value of the fixture -- return/yield of the fixture function (type variable).
+FixtureValue = TypeVar("FixtureValue", covariant=True)
+# The type of the fixture function (type alias).
+FixtureFunction = Callable[..., object]
+# The type of a fixture function (type alias generic in fixture value).
+_FixtureFunc = Callable[..., FixtureValue] | Callable[..., Generator[FixtureValue]]
+# The type of FixtureDef.cached_result (type alias generic in fixture value).
+_FixtureCachedResult = (
+ tuple[
+ # The result.
+ FixtureValue,
+ # Cache key.
+ object,
+ None,
+ ]
+ | tuple[
+ None,
+ # Cache key.
+ object,
+ # The exception and the original traceback.
+ tuple[BaseException, types.TracebackType | None],
+ ]
+)
+
+
+def pytest_sessionstart(session: Session) -> None:
+ session._fixturemanager = FixtureManager(session)
+
+
+def get_scope_package(
+ node: nodes.Item,
+ fixturedef: FixtureDef[object],
+) -> nodes.Node | None:
+ from _pytest.python import Package
+
+ for parent in node.iter_parents():
+ if isinstance(parent, Package):
+ if fixturedef.node is not None:
+ if parent == fixturedef.node:
+ return parent
+ else:
+ if parent.nodeid == fixturedef.baseid:
+ return parent
+ return node.session
+
+
+def is_visibility_more_specific(
+ candidate: FixtureDef[Any], other: FixtureDef[Any]
+) -> bool:
+ """Return whether the visibility of ``candidate`` is strictly more specific
+ than that of ``other``, i.e. ``candidate`` is defined on a strict descendant
+ in the collection tree of where ``other`` is defined."""
+ if candidate.node is None or other.node is None:
+ # Fallback for fixtures registered with a string nodeid (deprecated).
+ # In this case compare baseids, which are nodeid prefixes.
+ # This branch can be removed once baseid deprecation is done (pytest 10).
+ if candidate.baseid == other.baseid:
+ return False
+ if other.baseid == "":
+ return True
+ # `candidate.baseid` must continue with a node separator for it to be a
+ # true descendant.
+ return candidate.baseid.startswith(other.baseid) and candidate.baseid[
+ len(other.baseid)
+ ] in ("/", ":")
+
+ return (
+ candidate.node is not other.node and other.node in candidate.node.iter_parents()
+ )
+
+
+def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None:
+ """Get the closest parent node (including self) which matches the given
+ scope.
+
+ If there is no parent node for the scope (e.g. asking for class scope on a
+ Module, or on a Function when not defined in a class), returns None.
+ """
+ import _pytest.python
+
+ if scope is Scope.Function:
+ # Type ignored because this is actually safe, see:
+ # https://github.com/python/mypy/issues/4717
+ return node.getparent(nodes.Item) # type: ignore[type-abstract]
+ elif scope is Scope.Class:
+ return node.getparent(_pytest.python.Class)
+ elif scope is Scope.Module:
+ return node.getparent(_pytest.python.Module)
+ elif scope is Scope.Package:
+ return node.getparent(_pytest.python.Package)
+ elif scope is Scope.Session:
+ return node.getparent(_pytest.main.Session)
+ else:
+ assert_never(scope)
+
+
+# TODO: Try to use FixtureFunctionDefinition instead of the marker
+def getfixturemarker(obj: object) -> FixtureFunctionMarker | None:
+ """Return fixturemarker or None if it doesn't exist"""
+ if isinstance(obj, FixtureFunctionDefinition):
+ return obj._fixture_function_marker
+ return None
+
+
+# Algorithm for sorting on a per-parametrized resource setup basis.
+# It is called for Session scope first and performs sorting
+# down to the lower scopes such as to minimize number of "high scope"
+# setups and teardowns.
+
+
+@dataclasses.dataclass(frozen=True)
+class ParamArgKey:
+ """A key for a high-scoped parameter used by an item.
+
+ For use as a hashable key in `reorder_items`. The combination of fields
+ is meant to uniquely identify a particular "instance" of a param,
+ potentially shared by multiple items in a scope.
+ """
+
+ #: The param name.
+ argname: str
+ param_index: int
+ #: For scopes Package, Module, Class, the path to the file (directory in
+ #: Package's case) of the package/module/class where the item is defined.
+ scoped_item_path: Path | None
+ #: For Class scope, the class where the item is defined.
+ item_cls: type | None
+
+
+_V = TypeVar("_V")
+OrderedSet = dict[_V, None]
+
+
+def get_param_argkeys(item: nodes.Item, scope: Scope) -> Iterator[ParamArgKey]:
+ """Return all ParamArgKeys for item matching the specified high scope."""
+ assert scope is not Scope.Function
+
+ try:
+ callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
+ except AttributeError:
+ return
+
+ item_cls = None
+ if scope is Scope.Session:
+ scoped_item_path = None
+ elif scope is Scope.Package:
+ # Package key = module's directory.
+ scoped_item_path = item.path.parent
+ elif scope is Scope.Module:
+ scoped_item_path = item.path
+ elif scope is Scope.Class:
+ scoped_item_path = item.path
+ item_cls = item.cls # type: ignore[attr-defined]
+ else:
+ assert_never(scope)
+
+ for argname in callspec.indices:
+ if callspec._arg2scope[argname] != scope:
+ continue
+ param_index = callspec.indices[argname]
+ yield ParamArgKey(argname, param_index, scoped_item_path, item_cls)
+
+
+def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]:
+ argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[ParamArgKey]]] = {}
+ items_by_argkey: dict[Scope, dict[ParamArgKey, OrderedDict[nodes.Item, None]]] = {}
+ for scope in HIGH_SCOPES:
+ scoped_argkeys_by_item = argkeys_by_item[scope] = {}
+ scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
+ for item in items:
+ argkeys = dict.fromkeys(get_param_argkeys(item, scope))
+ if argkeys:
+ scoped_argkeys_by_item[item] = argkeys
+ for argkey in argkeys:
+ scoped_items_by_argkey[argkey][item] = None
+
+ items_set = dict.fromkeys(items)
+ return list(
+ reorder_items_atscope(
+ items_set, argkeys_by_item, items_by_argkey, Scope.Session
+ )
+ )
+
+
+def reorder_items_atscope(
+ items: OrderedSet[nodes.Item],
+ argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[ParamArgKey]]],
+ items_by_argkey: Mapping[
+ Scope, Mapping[ParamArgKey, OrderedDict[nodes.Item, None]]
+ ],
+ scope: Scope,
+) -> OrderedSet[nodes.Item]:
+ if scope is Scope.Function or len(items) < 3:
+ return items
+
+ scoped_items_by_argkey = items_by_argkey[scope]
+ scoped_argkeys_by_item = argkeys_by_item[scope]
+
+ ignore: set[ParamArgKey] = set()
+ items_deque = deque(items)
+ items_done: OrderedSet[nodes.Item] = {}
+ while items_deque:
+ no_argkey_items: OrderedSet[nodes.Item] = {}
+ slicing_argkey = None
+ while items_deque:
+ item = items_deque.popleft()
+ if item in items_done or item in no_argkey_items:
+ continue
+ argkeys = dict.fromkeys(
+ k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore
+ )
+ if not argkeys:
+ no_argkey_items[item] = None
+ else:
+ slicing_argkey, _ = argkeys.popitem()
+ # We don't have to remove relevant items from later in the
+ # deque because they'll just be ignored.
+ matching_items = [
+ i for i in scoped_items_by_argkey[slicing_argkey] if i in items
+ ]
+ for i in reversed(matching_items):
+ items_deque.appendleft(i)
+ # Fix items_by_argkey order.
+ for other_scope in HIGH_SCOPES:
+ other_scoped_items_by_argkey = items_by_argkey[other_scope]
+ for argkey in argkeys_by_item[other_scope].get(i, ()):
+ argkey_dict = other_scoped_items_by_argkey[argkey]
+ if not hasattr(sys, "pypy_version_info"):
+ argkey_dict[i] = None
+ argkey_dict.move_to_end(i, last=False)
+ else:
+ # Work around a bug in PyPy:
+ # https://github.com/pypy/pypy/issues/5257
+ # https://github.com/pytest-dev/pytest/issues/13312
+ bkp = argkey_dict.copy()
+ argkey_dict.clear()
+ argkey_dict[i] = None
+ argkey_dict.update(bkp)
+ break
+ if no_argkey_items:
+ reordered_no_argkey_items = reorder_items_atscope(
+ no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower()
+ )
+ items_done.update(reordered_no_argkey_items)
+ if slicing_argkey is not None:
+ ignore.add(slicing_argkey)
+ return items_done
+
+
+def traverse_fixture_closure(
+ initialnames: Iterable[str],
+ *,
+ getfixturedefs: Callable[[str], Sequence[FixtureDef[Any]] | None],
+) -> Iterator[str]:
+ """Statically traverse the fixture dependency closure in DFS order starting
+ from initialnames, yielding all requested fixture names (argnames).
+
+ Each argname is only yielded once.
+ """
+ # Track the index for each fixture name in the simulated stack.
+ # Needed for handling override chains correctly, similar to
+ # FixtureRequest._get_active_fixturedef.
+ # Using negative indices: -1 is the most specific (last), -2 is second to
+ # last, etc.
+ current_indices: dict[str, int] = {}
+
+ def process_argname(argname: str) -> Iterator[str]:
+ index = current_indices.get(argname)
+
+ # Optimization: already processed this argname.
+ if index == -1:
+ return
+
+ # Only yield each argname once.
+ if index is None:
+ yield argname
+ current_indices[argname] = -1
+
+ fixturedefs = getfixturedefs(argname)
+ if not fixturedefs:
+ return
+
+ index = current_indices.get(argname, -1)
+ if -index > len(fixturedefs):
+ # Exhausted the override chain (will error during runtest).
+ return
+ fixturedef = fixturedefs[index]
+
+ current_indices[argname] = index - 1
+ for dep in fixturedef.argnames:
+ yield from process_argname(dep)
+ current_indices[argname] = index
+
+ for argname in initialnames:
+ yield from process_argname(argname)
+
+
+@dataclasses.dataclass(frozen=True)
+class FuncFixtureInfo:
+ """Fixture-related information for a fixture-requesting item (e.g. test
+ function).
+
+ This is used to examine the fixtures which an item requests statically
+ (known during collection). This includes autouse fixtures, fixtures
+ requested by the `usefixtures` marker, fixtures requested in the function
+ parameters, and the transitive closure of these.
+
+ An item may also request fixtures dynamically (using `request.getfixturevalue`);
+ these are not reflected here.
+ """
+
+ __slots__ = ("argnames", "initialnames", "name2fixturedefs", "names_closure")
+
+ # Fixture names that the item requests directly by function parameters.
+ argnames: tuple[str, ...]
+ # Fixture names that the item immediately requires. These include
+ # argnames + fixture names specified via usefixtures and via autouse=True in
+ # fixture definitions.
+ initialnames: tuple[str, ...]
+ # The transitive closure of the fixture names that the item requires.
+ # Note: can't include dynamic dependencies (`request.getfixturevalue` calls).
+ names_closure: list[str]
+ # A map from a fixture name in the transitive closure to the FixtureDefs
+ # matching the name which are applicable to this function.
+ # There may be multiple overriding fixtures with the same name. The
+ # sequence is ordered from furthest to closes to the function.
+ name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]]
+
+ def prune_dependency_tree(self) -> None:
+ """Recompute names_closure from initialnames and name2fixturedefs.
+
+ Can only reduce names_closure, which means that the new closure will
+ always be a subset of the old one. The order is preserved.
+
+ This method is needed because direct parametrization may shadow some
+ of the fixtures that were included in the originally built dependency
+ tree. In this way the dependency tree can get pruned, and the closure
+ of argnames may get reduced.
+ """
+ closure = set(
+ traverse_fixture_closure(
+ self.initialnames,
+ getfixturedefs=self.name2fixturedefs.get,
+ )
+ )
+ self.names_closure[:] = (name for name in self.names_closure if name in closure)
+
+
+class FixtureRequest(abc.ABC):
+ """The type of the ``request`` fixture.
+
+ A request object gives access to the requesting test context and has a
+ ``param`` attribute in case the fixture is parametrized.
+ """
+
+ def __init__(
+ self,
+ pyfuncitem: Function,
+ fixturename: str | None,
+ arg2fixturedefs: Mapping[str, Sequence[FixtureDef[Any]]],
+ fixture_defs: dict[str, FixtureDef[Any]],
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ #: Fixture for which this request is being performed.
+ self.fixturename: Final = fixturename
+ self._pyfuncitem: Final = pyfuncitem
+ # The FixtureDefs for each fixture name statically requested by this
+ # item (computed during collection). Dynamically requested fixtures
+ # (using `request.getfixturevalue("foo")`) are not included here.
+ self._arg2fixturedefs: Final = arg2fixturedefs
+ # The evaluated argnames so far, mapping to the FixtureDef they resolved
+ # to.
+ self._fixture_defs: Final = fixture_defs
+ # Notes on the type of `param`:
+ # -`request.param` is only defined in parametrized fixtures, and will raise
+ # AttributeError otherwise. Python typing has no notion of "undefined", so
+ # this cannot be reflected in the type.
+ # - Technically `param` is only (possibly) defined on SubRequest, not
+ # FixtureRequest, but the typing of that is still in flux so this cheats.
+ # - In the future we might consider using a generic for the param type, but
+ # for now just using Any.
+ self.param: Any
+
+ @property
+ def _fixturemanager(self) -> FixtureManager:
+ return self._pyfuncitem.session._fixturemanager
+
+ @property
+ @abc.abstractmethod
+ def _scope(self) -> Scope:
+ raise NotImplementedError()
+
+ @property
+ def scope(self) -> ScopeName:
+ """Scope string, one of "function", "class", "module", "package", "session"."""
+ return self._scope.value
+
+ @abc.abstractmethod
+ def _check_scope(
+ self,
+ requested_fixturedef: FixtureDef[object],
+ requested_scope: Scope,
+ ) -> None:
+ raise NotImplementedError()
+
+ @property
+ def fixturenames(self) -> list[str]:
+ """Names of all active fixtures in this request."""
+ result = list(self._pyfuncitem.fixturenames)
+ result.extend(set(self._fixture_defs).difference(result))
+ return result
+
+ @property
+ @abc.abstractmethod
+ def node(self):
+ """Underlying collection node (depends on current request scope)."""
+ raise NotImplementedError()
+
+ @property
+ def config(self) -> Config:
+ """The pytest config object associated with this request."""
+ return self._pyfuncitem.config
+
+ @property
+ def function(self):
+ """Test function object if the request has a per-function scope."""
+ if self.scope != "function":
+ raise AttributeError(
+ f"function not available in {self.scope}-scoped context"
+ )
+ return self._pyfuncitem.obj
+
+ @property
+ def cls(self):
+ """Class (can be None) where the test function was collected."""
+ if self.scope not in ("class", "function"):
+ raise AttributeError(f"cls not available in {self.scope}-scoped context")
+ clscol = self._pyfuncitem.getparent(_pytest.python.Class)
+ if clscol:
+ return clscol.obj
+
+ @property
+ def instance(self):
+ """Instance (can be None) on which test function was collected."""
+ if self.scope != "function":
+ return None
+ return getattr(self._pyfuncitem, "instance", None)
+
+ @property
+ def module(self):
+ """Python module object where the test function was collected."""
+ if self.scope not in ("function", "class", "module"):
+ raise AttributeError(f"module not available in {self.scope}-scoped context")
+ mod = self._pyfuncitem.getparent(_pytest.python.Module)
+ assert mod is not None
+ return mod.obj
+
+ @property
+ def path(self) -> Path:
+ """Path where the test function was collected."""
+ if self.scope not in ("function", "class", "module", "package"):
+ raise AttributeError(f"path not available in {self.scope}-scoped context")
+ return self._pyfuncitem.path
+
+ @property
+ def keywords(self) -> MutableMapping[str, Any]:
+ """Keywords/markers dictionary for the underlying node."""
+ node: nodes.Node = self.node
+ return node.keywords
+
+ @property
+ def session(self) -> Session:
+ """Pytest session object."""
+ return self._pyfuncitem.session
+
+ @abc.abstractmethod
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ """Add finalizer/teardown function to be called without arguments after
+ the last test within the requesting test context finished execution."""
+ raise NotImplementedError()
+
+ def applymarker(self, marker: str | MarkDecorator) -> None:
+ """Apply a marker to a single test function invocation.
+
+ This method is useful if you don't want to have a keyword/marker
+ on all function invocations.
+
+ :param marker:
+ An object created by a call to ``pytest.mark.NAME(...)``.
+ """
+ self.node.add_marker(marker)
+
+ def raiseerror(self, msg: str | None) -> NoReturn:
+ """Raise a FixtureLookupError exception.
+
+ :param msg:
+ An optional custom error message.
+ """
+ raise FixtureLookupError(None, self, msg)
+
+ def _raise_teardown_lookup_error(self, argname: str) -> NoReturn:
+ msg = (
+ f'The fixture value for "{argname}" is not available during teardown '
+ "because it was not previously requested.\n"
+ "Only fixtures that were already active can be retrieved during teardown.\n"
+ "Request the fixture before teardown begins by declaring it in the fixture "
+ "signature or by calling request.getfixturevalue() before the fixture yields."
+ )
+ raise FixtureLookupError(argname, self, msg)
+
+ def getfixturevalue(self, argname: str) -> Any:
+ """Dynamically run a named fixture function.
+
+ Declaring fixtures via function argument is recommended where possible.
+ But if you can only decide whether to use another fixture at test
+ setup time, you may use this function to retrieve it inside a fixture
+ or test function body.
+
+ This method can be used during the test setup phase or the test run
+ phase. Avoid using it during the teardown phase.
+
+ .. versionchanged:: 9.1
+ Calling ``request.getfixturevalue()`` during teardown to request a
+ fixture that was not already requested
+ :ref:`is deprecated `.
+
+ :param argname:
+ The fixture name.
+ :raises pytest.FixtureLookupError:
+ If the given fixture could not be found.
+ """
+ # Note that in addition to the use case described in the docstring,
+ # getfixturevalue() is also called by pytest itself during item and fixture
+ # setup to evaluate the fixtures that are requested statically
+ # (using function parameters, autouse, etc).
+
+ fixturedef = self._get_active_fixturedef(argname)
+ assert fixturedef.cached_result is not None, (
+ f'The fixture value for "{argname}" is not available. '
+ "This can happen when the fixture has already been torn down."
+ )
+ return fixturedef.cached_result[0]
+
+ def _iter_chain(self) -> Iterator[SubRequest]:
+ """Yield all SubRequests in the chain, from self up.
+
+ Note: does *not* yield the TopRequest.
+ """
+ current = self
+ while isinstance(current, SubRequest):
+ yield current
+ current = current._parent_request
+
+ def _get_active_fixturedef(self, argname: str) -> FixtureDef[object]:
+ if argname == "request":
+ return RequestFixtureDef(self)
+
+ # If we already finished computing a fixture by this name in this item,
+ # return it.
+ fixturedef = self._fixture_defs.get(argname)
+ if fixturedef is not None:
+ self._check_scope(fixturedef, fixturedef._scope)
+ return fixturedef
+
+ # Find the appropriate fixturedef.
+ fixturedefs = self._arg2fixturedefs.get(argname, None)
+ if fixturedefs is None:
+ # We arrive here because of a dynamic call to
+ # getfixturevalue(argname) which was naturally
+ # not known at parsing/collection time.
+ fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
+ # No fixtures defined with this name.
+ if fixturedefs is None:
+ raise FixtureLookupError(argname, self)
+ # The are no fixtures with this name applicable for the function.
+ if not fixturedefs:
+ raise FixtureLookupError(argname, self)
+
+ # A fixture may override another fixture with the same name, e.g. a
+ # fixture in a module can override a fixture in a conftest, a fixture in
+ # a class can override a fixture in the module, and so on.
+ # An overriding fixture can request its own name (possibly indirectly);
+ # in this case it gets the value of the fixture it overrides, one level
+ # up.
+ # Check how many `argname`s deep we are, and take the next one.
+ # `fixturedefs` is sorted from furthest to closest, so use negative
+ # indexing to go in reverse.
+ index = -1
+ for request in self._iter_chain():
+ if request.fixturename == argname:
+ index -= 1
+ # If already consumed all of the available levels, fail.
+ if -index > len(fixturedefs):
+ raise FixtureLookupError(argname, self)
+ fixturedef = fixturedefs[index]
+
+ # Prepare a SubRequest object for calling the fixture.
+ try:
+ callspec = self._pyfuncitem.callspec
+ except AttributeError:
+ callspec = None
+ if callspec is not None and argname in callspec.params:
+ param = callspec.params[argname]
+ param_index = callspec.indices[argname]
+ # The parametrize invocation scope overrides the fixture's scope.
+ scope = callspec._arg2scope[argname]
+ else:
+ param = NOTSET
+ param_index = 0
+ scope = fixturedef._scope
+ self._check_fixturedef_without_param(fixturedef)
+ # The parametrize invocation scope only controls caching behavior while
+ # allowing wider-scoped fixtures to keep depending on the parametrized
+ # fixture. Scope control is enforced for parametrized fixtures
+ # by recreating the whole fixture tree on parameter change.
+ # Hence `fixturedef._scope`, not `scope`.
+ self._check_scope(fixturedef, fixturedef._scope)
+ subrequest = SubRequest(
+ self, scope, param, param_index, fixturedef, _ispytest=True
+ )
+
+ if not self.session._setupstate.is_node_active(self.node):
+ # TODO(pytest10.1): Remove the `warn` and `if` and call
+ # _raise_teardown_lookup_error unconditionally.
+ warnings.warn(
+ FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN.format(argname=argname),
+ stacklevel=3,
+ )
+ if subrequest.node not in self.session._setupstate.stack:
+ self._raise_teardown_lookup_error(argname)
+
+ # Make sure the fixture value is cached, running it if it isn't
+ fixturedef.execute(request=subrequest)
+
+ self._fixture_defs[argname] = fixturedef
+ return fixturedef
+
+ def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None:
+ """Check that this request is allowed to execute this fixturedef without
+ a param."""
+ funcitem = self._pyfuncitem
+ has_params = fixturedef.params is not None
+ fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
+ if has_params and fixtures_not_supported:
+ msg = (
+ f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
+ f"Node id: {funcitem.nodeid}\n"
+ f"Function type: {type(funcitem).__name__}"
+ )
+ fail(msg, pytrace=False)
+ if has_params:
+ frame = inspect.stack()[3]
+ frameinfo = inspect.getframeinfo(frame[0])
+ source_path = absolutepath(frameinfo.filename)
+ source_lineno = frameinfo.lineno
+ try:
+ source_path_str = str(source_path.relative_to(funcitem.config.rootpath))
+ except ValueError:
+ source_path_str = str(source_path)
+ location = getlocation(fixturedef.func, funcitem.config.rootpath)
+ msg = (
+ "The requested fixture has no parameter defined for test:\n"
+ f" {funcitem.nodeid}\n\n"
+ f"Requested fixture '{fixturedef.argname}' defined in:\n"
+ f"{location}\n\n"
+ f"Requested here:\n"
+ f"{source_path_str}:{source_lineno}"
+ )
+ fail(msg, pytrace=False)
+
+ def _get_fixturestack(self) -> list[FixtureDef[Any]]:
+ values = [request._fixturedef for request in self._iter_chain()]
+ values.reverse()
+ return values
+
+
+@final
+class TopRequest(FixtureRequest):
+ """The type of the ``request`` fixture in a test function."""
+
+ def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None:
+ super().__init__(
+ fixturename=None,
+ pyfuncitem=pyfuncitem,
+ arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs,
+ fixture_defs={},
+ _ispytest=_ispytest,
+ )
+
+ @property
+ def _scope(self) -> Scope:
+ return Scope.Function
+
+ def _check_scope(
+ self,
+ requested_fixturedef: FixtureDef[object],
+ requested_scope: Scope,
+ ) -> None:
+ # TopRequest always has function scope so always valid.
+ pass
+
+ @property
+ def node(self):
+ return self._pyfuncitem
+
+ def __repr__(self) -> str:
+ return f""
+
+ def _fillfixtures(self) -> None:
+ item = self._pyfuncitem
+ for argname in item.fixturenames:
+ if argname not in item.funcargs:
+ item.funcargs[argname] = self.getfixturevalue(argname)
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ self.node.addfinalizer(finalizer)
+
+
+@final
+class SubRequest(FixtureRequest):
+ """The type of the ``request`` fixture in a fixture function requested
+ (transitively) by a test function."""
+
+ def __init__(
+ self,
+ request: FixtureRequest,
+ scope: Scope,
+ param: Any,
+ param_index: int,
+ fixturedef: FixtureDef[object],
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ super().__init__(
+ pyfuncitem=request._pyfuncitem,
+ fixturename=fixturedef.argname,
+ fixture_defs=request._fixture_defs,
+ arg2fixturedefs=request._arg2fixturedefs,
+ _ispytest=_ispytest,
+ )
+ self._parent_request: Final[FixtureRequest] = request
+ self._fixturedef: Final[FixtureDef[object]] = fixturedef
+ if param is not NOTSET:
+ self.param = param
+ self.param_index: Final = param_index
+ self._scope_field: Final = scope
+ if scope is Scope.Function:
+ # This might also be a non-function Item despite its attribute name.
+ node: nodes.Node | None = self._pyfuncitem
+ elif scope is Scope.Package:
+ node = get_scope_package(self._pyfuncitem, self._fixturedef)
+ else:
+ node = get_scope_node(self._pyfuncitem, scope)
+ if node is None and scope is Scope.Class:
+ # Fallback to function item itself.
+ node = self._pyfuncitem
+ assert node, (
+ f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
+ )
+ self._node: Final = node
+
+ def __repr__(self) -> str:
+ return f""
+
+ @property
+ def _scope(self) -> Scope:
+ return self._scope_field
+
+ @property
+ def node(self):
+ return self._node
+
+ def _check_scope(
+ self,
+ requested_fixturedef: FixtureDef[object],
+ requested_scope: Scope,
+ ) -> None:
+ if self._scope > requested_scope:
+ # Try to report something helpful.
+ argname = requested_fixturedef.argname
+ fixture_stack = "\n".join(
+ self._format_fixturedef_line(fixturedef)
+ for fixturedef in self._get_fixturestack()
+ )
+ requested_fixture = self._format_fixturedef_line(requested_fixturedef)
+ fail(
+ f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
+ f"fixture {argname} with a {self._scope.value} scoped request object. "
+ f"Requesting fixture stack:\n{fixture_stack}\n"
+ f"Requested fixture:\n{requested_fixture}",
+ pytrace=False,
+ )
+
+ def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str:
+ factory = fixturedef.func
+ path, lineno = getfslineno(factory)
+ if isinstance(path, Path):
+ path = bestrelpath(self._pyfuncitem.session.path, path)
+ sig = signature(factory)
+ return f"{path}:{lineno + 1}: def {factory.__name__}{sig}"
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ self._fixturedef.addfinalizer(finalizer)
+
+
+@final
+class FixtureLookupError(LookupError):
+ """Could not return a requested fixture (missing or invalid)."""
+
+ def __init__(
+ self, argname: str | None, request: FixtureRequest, msg: str | None = None
+ ) -> None:
+ self.argname = argname
+ self.request = request
+ self.fixturestack = request._get_fixturestack()
+ self.msg = msg
+
+ def formatrepr(self) -> FixtureLookupErrorRepr:
+ tblines: list[str] = []
+ addline = tblines.append
+ stack = [self.request._pyfuncitem.obj]
+ stack.extend(map(lambda x: x.func, self.fixturestack))
+ msg = self.msg
+ if msg is not None and len(stack) > 1:
+ # The last fixture raise an error, let's present
+ # it at the requesting side.
+ stack = stack[:-1]
+ for function in stack:
+ fspath, lineno = getfslineno(function)
+ try:
+ lines, _ = inspect.getsourcelines(get_real_func(function))
+ except (OSError, IndexError, TypeError):
+ error_msg = "file %s, line %s: source code not available"
+ addline(error_msg % (fspath, lineno + 1))
+ else:
+ addline(f"file {fspath}, line {lineno + 1}")
+ for i, line in enumerate(lines):
+ line = line.rstrip()
+ addline(" " + line)
+ if line.lstrip().startswith("def"):
+ break
+
+ if msg is None:
+ fm = self.request._fixturemanager
+ available = set()
+ parent = self.request._pyfuncitem.parent
+ assert parent is not None
+ for name, fixturedefs in fm._arg2fixturedefs.items():
+ faclist = list(fm._matchfactories(fixturedefs, parent))
+ if faclist:
+ available.add(name)
+ if self.argname in available:
+ msg = (
+ f" recursive dependency involving fixture '{self.argname}' detected"
+ )
+ else:
+ msg = f"fixture '{self.argname}' not found"
+ msg += "\n available fixtures: {}".format(", ".join(sorted(available)))
+ msg += "\n use 'pytest --fixtures [testpath]' for help on them."
+
+ return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
+
+
+class FixtureLookupErrorRepr(TerminalRepr):
+ def __init__(
+ self,
+ filename: str | os.PathLike[str],
+ firstlineno: int,
+ tblines: Sequence[str],
+ errorstring: str,
+ argname: str | None,
+ ) -> None:
+ self.tblines = tblines
+ self.errorstring = errorstring
+ self.filename = filename
+ self.firstlineno = firstlineno
+ self.argname = argname
+
+ def toterminal(self, tw: TerminalWriter) -> None:
+ # tw.line("FixtureLookupError: %s" %(self.argname), red=True)
+ for tbline in self.tblines:
+ tw.line(tbline.rstrip())
+ lines = self.errorstring.split("\n")
+ if lines:
+ tw.line(
+ f"{ExceptionInfoFormatter.fail_marker} {lines[0].strip()}",
+ red=True,
+ )
+ for line in lines[1:]:
+ tw.line(
+ f"{ExceptionInfoFormatter.flow_marker} {line.strip()}",
+ red=True,
+ )
+ tw.line()
+ tw.line(f"{os.fspath(self.filename)}:{self.firstlineno + 1}")
+
+
+def call_fixture_func(
+ fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
+) -> FixtureValue:
+ if inspect.isgeneratorfunction(fixturefunc):
+ fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc)
+ generator = fixturefunc(**kwargs)
+ try:
+ fixture_result = next(generator)
+ except StopIteration:
+ raise ValueError(f"{request.fixturename} did not yield a value") from None
+ finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
+ request.addfinalizer(finalizer)
+ else:
+ fixturefunc = cast(Callable[..., FixtureValue], fixturefunc)
+ fixture_result = fixturefunc(**kwargs)
+ return fixture_result
+
+
+def _teardown_yield_fixture(fixturefunc, it) -> None:
+ """Execute the teardown of a fixture function by advancing the iterator
+ after the yield and ensure the iteration ends (if not it means there is
+ more than one yield in the function)."""
+ try:
+ next(it)
+ except StopIteration:
+ pass
+ else:
+ fs, lineno = getfslineno(fixturefunc)
+ fail(
+ f"fixture function has more than one 'yield':\n\n"
+ f"{Source(fixturefunc).indent()}\n"
+ f"{fs}:{lineno + 1}",
+ pytrace=False,
+ )
+
+
+def _eval_scope_callable(
+ scope_callable: Callable[[str, Config], ScopeName],
+ fixture_name: str,
+ config: Config,
+) -> ScopeName:
+ try:
+ # Type ignored because there is no typing mechanism to specify
+ # keyword arguments, currently.
+ result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
+ except Exception as e:
+ raise TypeError(
+ f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n"
+ "Expected a function with the signature (*, fixture_name, config)"
+ ) from e
+ if not isinstance(result, str):
+ fail(
+ f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n"
+ f"{result!r}",
+ pytrace=False,
+ )
+ return result
+
+
+class FixtureDef(Generic[FixtureValue]):
+ """A container for a fixture definition.
+
+ Note: At this time, only explicitly documented fields and methods are
+ considered public stable API.
+ """
+
+ def __init__(
+ self,
+ config: Config,
+ baseid: str | None | NotSetType,
+ argname: str,
+ func: _FixtureFunc[FixtureValue],
+ scope: Scope | ScopeName | Callable[[str, Config], ScopeName] | None,
+ params: Sequence[object] | None,
+ ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
+ *,
+ node: nodes.Node | NotSetType = NOTSET,
+ # only used in a deprecationwarning msg, can be removed in pytest9
+ _autouse: bool = False,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ # Emit deprecation warning if deprecated baseid string is used.
+ if node is NOTSET:
+ warnings.warn(FIXTURE_BASEID_DEPRECATED, stacklevel=2)
+ if baseid is NOTSET:
+ baseid = None
+ # The node where this fixture was defined, if available.
+ # Used for node-based matching which is more robust than string matching.
+ self.node: Final = node if node is not NOTSET else None
+ # The "base" node ID for the fixture.
+ #
+ # This is a node ID prefix. A fixture is only available to a node (e.g.
+ # a `Function` item) if the fixture's baseid is a nodeid of a parent of
+ # node.
+ #
+ # For a fixture found in a Collector's object (e.g. a `Module`s module,
+ # a `Class`'s class), the baseid is the Collector's nodeid.
+ #
+ # For a fixture found in a conftest plugin, the baseid is the conftest's
+ # directory path relative to the rootdir.
+ #
+ # For other plugins, the baseid is the empty string (always matches).
+ # When node is available, baseid is derived from node.nodeid.
+ #
+ # Deprecated: replaced by ``node``.
+ self.baseid: Final = node.nodeid if node is not NOTSET else (baseid or "")
+ # Whether the fixture was found from a node or a conftest in the
+ # collection tree. Will be false for fixtures defined in non-conftest
+ # plugins.
+ #
+ # Deprecated: kept only to back the deprecated ``has_location`` property.
+ self._has_location: Final = node is not NOTSET or baseid is not None
+ # The fixture factory function.
+ self.func: Final = func
+ # The name by which the fixture may be requested.
+ self.argname: Final = argname
+ if scope is None:
+ scope = Scope.Function
+ elif callable(scope):
+ scope = _eval_scope_callable(scope, argname, config)
+ if isinstance(scope, str):
+ scope = Scope.from_user(
+ scope, descr=f"Fixture '{func.__name__}'", where=self.baseid
+ )
+ self._scope: Final = scope
+ # If the fixture is directly parametrized, the parameter values.
+ self.params: Final = params
+ # If the fixture is directly parametrized, a tuple of explicit IDs to
+ # assign to the parameter values, or a callable to generate an ID given
+ # a parameter value.
+ self.ids: Final = ids
+ # The names requested by the fixtures.
+ self.argnames: Final = getfuncargnames(func, name=argname)
+ # If the fixture was executed, the current value of the fixture.
+ # Can change if the fixture is executed with different parameters.
+ self.cached_result: _FixtureCachedResult[FixtureValue] | None = None
+ self._finalizers: Final[list[Callable[[], object]]] = []
+
+ # only used to emit a deprecationwarning, can be removed in pytest9
+ self._autouse = _autouse
+
+ @property
+ def scope(self) -> ScopeName:
+ """Scope string, one of "function", "class", "module", "package", "session"."""
+ return self._scope.value
+
+ @property
+ def has_location(self) -> bool:
+ warnings.warn(FIXTUREDEF_HAS_LOCATION_DEPRECATED, stacklevel=2)
+ return self._has_location
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ self._finalizers.append(finalizer)
+
+ def finish(self, request: SubRequest) -> None:
+ if self.cached_result is None:
+ # Already finished. It is assumed that finalizers cannot be added in
+ # this state.
+ return
+
+ exceptions: list[BaseException] = []
+ while self._finalizers:
+ fin = self._finalizers.pop()
+ try:
+ fin()
+ except BaseException as e:
+ exceptions.append(e)
+ node = request.node
+ # Even if finalization fails, we invalidate the cached fixture
+ # value and remove all finalizers because they may be bound methods
+ # which will keep instances alive.
+ self.cached_result = None
+ self._finalizers.clear()
+ if len(exceptions) == 1:
+ raise exceptions[0]
+ elif len(exceptions) > 1:
+ msg = f'errors while tearing down fixture "{self.argname}" of {node}'
+ raise BaseExceptionGroup(msg, exceptions[::-1])
+
+ def execute(self, request: SubRequest) -> FixtureValue:
+ """Return the value of this fixture, executing it if not cached."""
+ # Ensure that the dependent fixtures requested by this fixture are loaded.
+ # This needs to be done before checking if we have a cached value, since
+ # if a dependent fixture has their cache invalidated, e.g. due to
+ # parametrization, they finalize themselves and fixtures depending on it
+ # (which will likely include this fixture) setting `self.cached_result = None`.
+ # See #4871
+ requested_fixtures_that_should_finalize_us = []
+ for argname in self.argnames:
+ fixturedef = request._get_active_fixturedef(argname)
+ # Saves requested fixtures in a list so we later can add our finalizer
+ # to them, ensuring that if a requested fixture gets torn down we get torn
+ # down first. This is generally handled by SetupState, but still currently
+ # needed when this fixture is not parametrized but depends on a parametrized
+ # fixture.
+ requested_fixtures_that_should_finalize_us.append(fixturedef)
+
+ # Check for (and return) cached value/exception.
+ if self.cached_result is not None:
+ request_cache_key = self.cache_key(request)
+ cache_key = self.cached_result[1]
+ try:
+ # Attempt to make a normal == check: this might fail for objects
+ # which do not implement the standard comparison (like numpy arrays -- #6497).
+ cache_hit = bool(request_cache_key == cache_key)
+ except (ValueError, RuntimeError):
+ # If the comparison raises, use 'is' as fallback.
+ cache_hit = request_cache_key is cache_key
+
+ if cache_hit:
+ if self.cached_result[2] is not None:
+ exc, exc_tb = self.cached_result[2]
+ raise exc.with_traceback(exc_tb)
+ else:
+ return self.cached_result[0]
+ # We have a previous but differently parametrized fixture instance
+ # so we need to tear it down before creating a new one.
+ self.finish(request)
+ assert self.cached_result is None
+
+ # Add finalizer to requested fixtures we saved previously.
+ # We make sure to do this after checking for cached value to avoid
+ # adding our finalizer multiple times. (#12135)
+ finalizer = functools.partial(self.finish, request=request)
+ for parent_fixture in requested_fixtures_that_should_finalize_us:
+ parent_fixture.addfinalizer(finalizer)
+
+ # Register the pytest_fixture_post_finalizer as the first finalizer,
+ # which is executed last.
+ assert not self._finalizers
+ self.addfinalizer(
+ lambda: request.node.ihook.pytest_fixture_post_finalizer(
+ fixturedef=self, request=request
+ )
+ )
+
+ ihook = request.node.ihook
+ try:
+ # Setup the fixture, run the code in it, and cache the value
+ # in self.cached_result.
+ result: FixtureValue = ihook.pytest_fixture_setup(
+ fixturedef=self, request=request
+ )
+ finally:
+ # Schedule our finalizer, even if the setup failed.
+ request.node.addfinalizer(finalizer)
+
+ return result
+
+ def cache_key(self, request: SubRequest) -> object:
+ return getattr(request, "param", None)
+
+ def __repr__(self) -> str:
+ return f""
+
+
+class RequestFixtureDef(FixtureDef[FixtureRequest]):
+ """A custom FixtureDef for the special "request" fixture.
+
+ A new one is generated on-demand whenever "request" is requested.
+ """
+
+ def __init__(self, request: FixtureRequest) -> None:
+ super().__init__(
+ config=request.config,
+ baseid=NOTSET,
+ argname="request",
+ func=lambda: request,
+ scope=Scope.Function,
+ params=None,
+ node=request.node,
+ _ispytest=True,
+ )
+ self.cached_result = (request, [0], None)
+
+ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
+ pass
+
+
+def resolve_fixture_function(
+ fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
+) -> _FixtureFunc[FixtureValue]:
+ """Get the actual callable that can be called to obtain the fixture
+ value."""
+ fixturefunc = fixturedef.func
+ # The fixture function needs to be bound to the actual
+ # request.instance so that code working with "fixturedef" behaves
+ # as expected.
+ instance = request.instance
+
+ if fixturedef._scope is Scope.Class:
+ # Check if fixture is an instance method (bound to instance, not class)
+ if hasattr(fixturefunc, "__self__"):
+ bound_to = fixturefunc.__self__
+ # classmethod: bound_to is the class itself (a type)
+ # instance method: bound_to is an instance (not a type)
+ if not isinstance(bound_to, type):
+ warnings.warn(CLASS_FIXTURE_INSTANCE_METHOD, stacklevel=2)
+
+ if instance is not None:
+ # Handle the case where fixture is defined not in a test class, but some other class
+ # (for example a plugin class with a fixture), see #2270.
+ if hasattr(fixturefunc, "__self__") and not isinstance(
+ instance,
+ fixturefunc.__self__.__class__,
+ ):
+ return fixturefunc
+ fixturefunc = getimfunc(fixturedef.func)
+ if fixturefunc != fixturedef.func:
+ fixturefunc = fixturefunc.__get__(instance)
+ return fixturefunc
+
+
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[FixtureValue], request: SubRequest
+) -> FixtureValue:
+ """Execution of fixture setup."""
+ kwargs = {}
+ for argname in fixturedef.argnames:
+ kwargs[argname] = request.getfixturevalue(argname)
+
+ fixturefunc = resolve_fixture_function(fixturedef, request)
+ my_cache_key = fixturedef.cache_key(request)
+
+ if inspect.isasyncgenfunction(fixturefunc) or inspect.iscoroutinefunction(
+ fixturefunc
+ ):
+ auto_str = " with autouse=True" if fixturedef._autouse else ""
+ fail(
+ f"{request.node.name!r} requested an async fixture {request.fixturename!r}{auto_str}, "
+ "with no plugin or hook that handled it. This is an error, as pytest does not natively support it.\n"
+ "See: https://docs.pytest.org/en/stable/deprecations.html#sync-test-depending-on-async-fixture",
+ pytrace=False,
+ )
+
+ try:
+ result = call_fixture_func(fixturefunc, request, kwargs)
+ except TEST_OUTCOME as e:
+ if isinstance(e, skip.Exception):
+ # The test requested a fixture which caused a skip.
+ # Don't show the fixture as the skip location, as then the user
+ # wouldn't know which test skipped.
+ e._use_item_location = True
+ fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__))
+ raise
+ fixturedef.cached_result = (result, my_cache_key, None)
+ return result
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class FixtureFunctionMarker:
+ scope: ScopeName | Callable[[str, Config], ScopeName]
+ params: tuple[object, ...] | None
+ autouse: bool = False
+ ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None
+ name: str | None = None
+
+ _ispytest: dataclasses.InitVar[bool] = False
+
+ def __post_init__(self, _ispytest: bool) -> None:
+ check_ispytest(_ispytest)
+
+ def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition:
+ if inspect.isclass(function):
+ raise ValueError("class fixtures not supported (maybe in the future)")
+
+ if isinstance(function, FixtureFunctionDefinition):
+ raise ValueError(
+ f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
+ )
+
+ if hasattr(function, "pytestmark"):
+ fail(
+ "Marks cannot be applied to fixtures.\n"
+ "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
+ )
+
+ fixture_definition = FixtureFunctionDefinition(
+ function=function, fixture_function_marker=self, _ispytest=True
+ )
+
+ name = self.name or function.__name__
+ if name == "request":
+ location = getlocation(function)
+ fail(
+ f"'request' is a reserved word for fixtures, use another name:\n {location}",
+ pytrace=False,
+ )
+
+ return fixture_definition
+
+
+# TODO: paramspec/return type annotation tracking and storing
+class FixtureFunctionDefinition:
+ def __init__(
+ self,
+ *,
+ function: Callable[..., Any],
+ fixture_function_marker: FixtureFunctionMarker,
+ instance: object | None = None,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self.name = fixture_function_marker.name or function.__name__
+ # In order to show the function that this fixture contains in messages.
+ # Set the __name__ to be same as the function __name__ or the given fixture name.
+ self.__name__ = self.name
+ self._fixture_function_marker = fixture_function_marker
+ if instance is not None:
+ self._fixture_function = cast(
+ Callable[..., Any], function.__get__(instance)
+ )
+ else:
+ self._fixture_function = function
+ functools.update_wrapper(self, function)
+
+ def __repr__(self) -> str:
+ return f""
+
+ def __get__(self, instance, owner=None):
+ """Behave like a method if the function it was applied to was a method."""
+ return FixtureFunctionDefinition(
+ function=self._fixture_function,
+ fixture_function_marker=self._fixture_function_marker,
+ instance=instance,
+ _ispytest=True,
+ )
+
+ def __call__(self, *args: Any, **kwds: Any) -> Any:
+ message = (
+ f'Fixture "{self.name}" called directly. Fixtures are not meant to be called directly,\n'
+ "but are created automatically when test functions request them as parameters.\n"
+ "See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
+ "https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly"
+ )
+ fail(message, pytrace=False)
+
+ def _get_wrapped_function(self) -> Callable[..., Any]:
+ return self._fixture_function
+
+
+@overload
+def fixture(
+ fixture_function: Callable[..., object],
+ *,
+ scope: ScopeName | Callable[[str, Config], ScopeName] = ...,
+ params: Iterable[object] | None = ...,
+ autouse: bool = ...,
+ ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
+ name: str | None = ...,
+) -> FixtureFunctionDefinition: ...
+
+
+@overload
+def fixture(
+ fixture_function: None = ...,
+ *,
+ scope: ScopeName | Callable[[str, Config], ScopeName] = ...,
+ params: Iterable[object] | None = ...,
+ autouse: bool = ...,
+ ids: Sequence[object | None] | Callable[[Any], object | None] | None = ...,
+ name: str | None = None,
+) -> FixtureFunctionMarker: ...
+
+
+def fixture(
+ fixture_function: FixtureFunction | None = None,
+ *,
+ scope: ScopeName | Callable[[str, Config], ScopeName] = "function",
+ params: Iterable[object] | None = None,
+ autouse: bool = False,
+ ids: Sequence[object | None] | Callable[[Any], object | None] | None = None,
+ name: str | None = None,
+) -> FixtureFunctionMarker | FixtureFunctionDefinition:
+ """Decorator to mark a fixture factory function.
+
+ This decorator can be used, with or without parameters, to define a
+ fixture function.
+
+ The name of the fixture function can later be referenced to cause its
+ invocation ahead of running tests: test modules or classes can use the
+ ``pytest.mark.usefixtures(fixturename)`` marker.
+
+ Test functions can directly use fixture names as input arguments in which
+ case the fixture instance returned from the fixture function will be
+ injected.
+
+ Fixtures can provide their values to test functions using ``return`` or
+ ``yield`` statements. When using ``yield`` the code block after the
+ ``yield`` statement is executed as teardown code regardless of the test
+ outcome, and must yield exactly once.
+
+ :param scope:
+ The scope for which this fixture is shared; one of ``"function"``
+ (default), ``"class"``, ``"module"``, ``"package"`` or ``"session"``.
+
+ This parameter may also be a callable which receives ``(fixture_name, config)``
+ as parameters, and must return a ``str`` with one of the values mentioned above.
+
+ See :ref:`dynamic scope` in the docs for more information.
+
+ :param params:
+ An optional list of parameters which will cause multiple invocations
+ of the fixture function and all of the tests using it. The current
+ parameter is available in ``request.param``.
+
+ :param autouse:
+ If True, the fixture func is activated for all tests that can see it.
+ If False (the default), an explicit reference is needed to activate
+ the fixture.
+
+ :param ids:
+ Sequence of ids each corresponding to the params so that they are
+ part of the test id. If no ids are provided they will be generated
+ automatically from the params.
+
+ :param name:
+ The name of the fixture. This defaults to the name of the decorated
+ function. If a fixture is used in the same module in which it is
+ defined, the function name of the fixture will be shadowed by the
+ function arg that requests the fixture; one way to resolve this is to
+ name the decorated function ``fixture_`` and then use
+ ``@pytest.fixture(name='')``.
+ """
+ fixture_marker = FixtureFunctionMarker(
+ scope=scope,
+ params=tuple(params) if params is not None else None,
+ autouse=autouse,
+ ids=None if ids is None else ids if callable(ids) else tuple(ids),
+ name=name,
+ _ispytest=True,
+ )
+
+ # Direct decoration.
+ if fixture_function:
+ return fixture_marker(fixture_function)
+
+ return fixture_marker
+
+
+@deprecated(
+ "@pytest.yield_fixture is deprecated. Use @pytest.fixture instead; they are the same.",
+ category=None, # We have our own runtime warning logic
+)
+def yield_fixture(
+ fixture_function=None,
+ *args,
+ scope="function",
+ params=None,
+ autouse=False,
+ ids=None,
+ name=None,
+):
+ """(Return a) decorator to mark a yield-fixture factory function.
+
+ .. deprecated:: 3.0
+ Use :py:func:`pytest.fixture` directly instead.
+ """
+ warnings.warn(YIELD_FIXTURE, stacklevel=2)
+ return fixture(
+ fixture_function,
+ *args,
+ scope=scope,
+ params=params,
+ autouse=autouse,
+ ids=ids,
+ name=name,
+ )
+
+
+@fixture(scope="session")
+def pytestconfig(request: FixtureRequest) -> Config:
+ """Session-scoped fixture that returns the session's :class:`pytest.Config`
+ object.
+
+ Example::
+
+ def test_foo(pytestconfig):
+ if pytestconfig.get_verbosity() > 0:
+ ...
+
+ """
+ return request.config
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "usefixtures",
+ type="args",
+ default=[],
+ help="List of default fixtures to be used with this project",
+ )
+ group = parser.getgroup("general")
+ group.addoption(
+ "--fixtures",
+ "--funcargs",
+ action="store_true",
+ dest="showfixtures",
+ default=False,
+ help="Show available fixtures, sorted by plugin appearance "
+ "(fixtures with leading '_' are only shown with '-v')",
+ )
+ group.addoption(
+ "--fixtures-per-test",
+ action="store_true",
+ dest="show_fixtures_per_test",
+ default=False,
+ help="Show fixtures per test",
+ )
+
+
+def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
+ if config.option.showfixtures:
+ showfixtures(config)
+ return 0
+ if config.option.show_fixtures_per_test:
+ show_fixtures_per_test(config)
+ return 0
+ return None
+
+
+def _resolve_args_directness(
+ argnames: Sequence[str],
+ indirect: bool | Sequence[str],
+ nodeid: str,
+) -> dict[str, Literal["indirect", "direct"]]:
+ """Resolve if each parametrized argument must be considered an indirect
+ parameter to a fixture of the same name, or a direct parameter to the
+ parametrized function, based on the ``indirect`` parameter of the
+ parametrize() call.
+
+ :param argnames:
+ List of argument names passed to ``parametrize()``.
+ :param indirect:
+ Same as the ``indirect`` parameter of ``parametrize()``.
+ :param nodeid:
+ Node ID to which the parametrization is applied.
+ :returns:
+ A dict mapping each arg name to either "indirect" or "direct".
+ """
+ arg_directness: dict[str, Literal["indirect", "direct"]]
+ if isinstance(indirect, bool):
+ arg_directness = dict.fromkeys(argnames, "indirect" if indirect else "direct")
+ elif isinstance(indirect, Sequence):
+ arg_directness = dict.fromkeys(argnames, "direct")
+ for arg in indirect:
+ if arg not in argnames:
+ fail(
+ f"In {nodeid}: indirect fixture '{arg}' doesn't exist",
+ pytrace=False,
+ )
+ arg_directness[arg] = "indirect"
+ else:
+ fail(
+ f"In {nodeid}: expected Sequence or boolean for indirect, got {type(indirect).__name__}",
+ pytrace=False,
+ )
+ return arg_directness
+
+
+def _get_direct_parametrize_args(node: nodes.Node) -> set[str]:
+ """Return all direct parametrization arguments of a node, so we don't
+ mistake them for fixtures.
+
+ Check https://github.com/pytest-dev/pytest/issues/5036.
+
+ These things are done later as well when dealing with parametrization
+ so this could be improved.
+ """
+ parametrize_argnames: set[str] = set()
+ for marker in node.iter_markers(name="parametrize"):
+ indirect = marker.kwargs.get("indirect", False)
+ p_argnames, _ = ParameterSet._parse_parametrize_args(
+ *marker.args, **marker.kwargs
+ )
+ p_directness = _resolve_args_directness(p_argnames, indirect, node.nodeid)
+ parametrize_argnames.update(
+ argname
+ for argname, directness in p_directness.items()
+ if directness == "direct"
+ )
+ return parametrize_argnames
+
+
+def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]:
+ """De-duplicate the sequence of names while keeping the original order."""
+ # Ideally we would use a set, but it does not preserve insertion order.
+ return tuple(dict.fromkeys(name for seq in seqs for name in seq))
+
+
+class FixtureManager:
+ """pytest fixture definitions and information is stored and managed
+ from this class.
+
+ During collection fm.parsefactories() is called multiple times to parse
+ fixture function definitions into FixtureDef objects and internal
+ data structures.
+
+ During collection of test functions, metafunc-mechanics instantiate
+ a FuncFixtureInfo object which is cached per node/func-name.
+ This FuncFixtureInfo object is later retrieved by Function nodes
+ which themselves offer a fixturenames attribute.
+
+ The FuncFixtureInfo object holds information about fixtures and FixtureDefs
+ relevant for a particular function. An initial list of fixtures is
+ assembled like this:
+
+ - config-defined usefixtures
+ - autouse-marked fixtures along the collection chain up from the function
+ - usefixtures markers at module/class/function level
+ - test function funcargs
+
+ Subsequently the funcfixtureinfo.fixturenames attribute is computed
+ as the closure of the fixtures needed to setup the initial fixtures,
+ i.e. fixtures needed by fixture functions themselves are appended
+ to the fixturenames list.
+
+ Upon the test-setup phases all fixturenames are instantiated, retrieved
+ by a lookup of their FuncFixtureInfo.
+ """
+
+ def __init__(self, session: Session) -> None:
+ self.session = session
+ self.config: Config = session.config
+ # Maps a fixture name (argname) to all of the FixtureDefs in the test
+ # suite/plugins defined with this name. Populated by parsefactories().
+ # TODO: The order of the FixtureDefs list of each arg is significant,
+ # explain.
+ self._arg2fixturedefs: Final[dict[str, list[FixtureDef[Any]]]] = {}
+ # A mapping from a node to a list of autouse fixture names it defines.
+ # The Session entry holds global usefixtures from config.
+ self._node_autousenames: Final[dict[nodes.Node, list[str]]] = {
+ session: list(self.config.getini("usefixtures")),
+ }
+ # Legacy fallback: nodeid string -> autouse names, for plugins still
+ # using the deprecated nodeid-based API without a node reference.
+ self._nodeid_autousenames: Final[dict[str, list[str]]] = {}
+ # Pending conftest modules waiting to be parsed when their Directory is collected.
+ # Maps directory path -> conftest plugin module.
+ self._pending_conftests: Final[dict[Path, object]] = {}
+ session.config.pluginmanager.register(self, "funcmanage")
+ # Flush initial conftests from directories above rootpath immediately.
+ # These will never get a Directory collector, so they need Session scope.
+ # This must happen here (not in pytest_make_collect_report) because
+ # collection may fail before Session collection starts (e.g. bad args).
+ self._flush_pending_conftests_to_session(session)
+
+ def getfixtureinfo(
+ self,
+ node: nodes.Item,
+ func: Callable[..., object] | None,
+ cls: type | None,
+ ) -> FuncFixtureInfo:
+ """Calculate the :class:`FuncFixtureInfo` for an item.
+
+ If ``func`` is None, or if the item sets an attribute
+ ``nofuncargs = True``, then ``func`` is not examined at all.
+
+ :param node:
+ The item requesting the fixtures.
+ :param func:
+ The item's function.
+ :param cls:
+ If the function is a method, the method's class.
+ """
+ if func is not None and not getattr(node, "nofuncargs", False):
+ argnames = getfuncargnames(func, name=node.name, cls=cls)
+ else:
+ argnames = ()
+ usefixturesnames = self._getusefixturesnames(node)
+ autousenames = self._getautousenames(node)
+ initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
+
+ direct_parametrize_args = _get_direct_parametrize_args(node)
+
+ names_closure, arg2fixturedefs = self.getfixtureclosure(
+ parentnode=node,
+ initialnames=initialnames,
+ ignore_args=direct_parametrize_args,
+ )
+
+ return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
+
+ def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None:
+ # Fixtures defined in conftest plugins are only visible to within the
+ # conftest's directory. This is unlike fixtures in non-conftest plugins
+ # which have global visibility. Conftest fixtures are deferred until
+ # their Directory is collected, so we can use the Directory's nodeid.
+ if plugin_name and plugin_name.endswith("conftest.py"):
+ # Note: we explicitly do *not* use `plugin.__file__` here -- The
+ # difference is that plugin_name has the correct capitalization on
+ # case-insensitive systems (Windows) and other normalization issues
+ # (issue #11816).
+ conftestpath = absolutepath(plugin_name)
+ conftest_dir = conftestpath.parent
+ # Store conftest for deferred parsing when its Directory is collected.
+ self._pending_conftests[conftest_dir] = plugin
+ else:
+ # Non-conftest plugins have global visibility.
+ self.parsefactories(holder=plugin, node=self.session)
+
+ @hookimpl(wrapper=True)
+ def pytest_make_collect_report(
+ self, collector: nodes.Collector
+ ) -> Generator[None, CollectReport, CollectReport]:
+ result = yield
+ if isinstance(collector, nodes.Directory):
+ plugin = self._pending_conftests.pop(collector.path, None)
+ if plugin is not None:
+ self.parsefactories(holder=plugin, node=collector)
+ return result
+
+ def _flush_pending_conftests_to_session(self, session: Session) -> None:
+ """Assign Session scope to initial conftests whose directories won't
+ be collected as Directory nodes (e.g. ancestors above rootdir)."""
+ rootpath = session.config.rootpath
+ orphaned: list[tuple[Path, object]] = []
+ for conftest_dir, plugin in list(self._pending_conftests.items()):
+ # If the conftest dir is not under rootpath, it will never get
+ # a Directory collector — assign it to Session now.
+ try:
+ conftest_dir.relative_to(rootpath)
+ except ValueError:
+ orphaned.append((conftest_dir, plugin))
+ for conftest_dir, plugin in orphaned:
+ del self._pending_conftests[conftest_dir]
+ self.parsefactories(holder=plugin, node=session)
+
+ def pytest_collection_finish(self) -> None:
+ """Clean up any conftests that were never collected by a Directory.
+
+ After __init__ flushes above-rootdir conftests and collection pops
+ under-rootdir ones, remaining entries mean collection was interrupted
+ (e.g. UsageError for a bad path). These conftests' fixtures aren't
+ needed since their directories' tests weren't collected either.
+ """
+ self._pending_conftests.clear()
+
+ def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
+ """Return the names of autouse fixtures applicable to node."""
+ for parentnode in node.listchain():
+ basenames = self._node_autousenames.get(parentnode)
+ if basenames:
+ yield from basenames
+ # Legacy fallback: check string-based nodeid autouse names.
+ nodeid_basenames = self._nodeid_autousenames.get(parentnode.nodeid)
+ if nodeid_basenames:
+ yield from nodeid_basenames
+
+ def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
+ """Return the names of usefixtures fixtures applicable to node."""
+ for marker_node, mark in node.iter_markers_with_node(name="usefixtures"):
+ if not mark.args:
+ marker_node.warn(
+ PytestWarning(
+ f"usefixtures() in {node.nodeid} without arguments has no effect"
+ )
+ )
+ yield from mark.args
+
+ def getfixtureclosure(
+ self,
+ parentnode: nodes.Node,
+ initialnames: tuple[str, ...],
+ ignore_args: AbstractSet[str],
+ ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]:
+ # Collect the closure of all fixtures, starting with the given
+ # fixturenames as the initial set. As we have to visit all
+ # factory definitions anyway, we also return an arg2fixturedefs
+ # mapping so that the caller can reuse it and does not have
+ # to re-discover fixturedefs again for each fixturename
+ # (discovering matching fixtures for a given name/node is expensive).
+
+ arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {}
+
+ def getfixturedefs(argname: str) -> Sequence[FixtureDef[Any]] | None:
+ if argname in ignore_args:
+ return None
+
+ fixturedefs = arg2fixturedefs.get(argname)
+ if not fixturedefs:
+ fixturedefs = self.getfixturedefs(argname, parentnode)
+ if not fixturedefs:
+ # Fixture not defined or not visible (will error during runtest).
+ return None
+ arg2fixturedefs[argname] = fixturedefs
+ return fixturedefs
+
+ def sort_by_scope(arg_name: str) -> Scope:
+ try:
+ fixturedefs = arg2fixturedefs[arg_name]
+ except KeyError:
+ return Scope.Function
+ else:
+ return fixturedefs[-1]._scope
+
+ fixturenames_closure = sorted(
+ traverse_fixture_closure(
+ initialnames,
+ getfixturedefs=getfixturedefs,
+ ),
+ key=sort_by_scope,
+ reverse=True,
+ )
+
+ return fixturenames_closure, arg2fixturedefs
+
+ def pytest_generate_tests(self, metafunc: Metafunc) -> None:
+ """Generate new tests based on parametrized fixtures used by the given metafunc"""
+
+ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]:
+ args, _ = ParameterSet._parse_parametrize_args(*mark.args, **mark.kwargs)
+ return args
+
+ for argname in metafunc.fixturenames:
+ # Get the FixtureDefs for the argname.
+ fixture_defs = metafunc._arg2fixturedefs.get(argname, ())
+
+ # If the test itself parametrizes using this argname, give it
+ # precedence.
+ if any(
+ argname in get_parametrize_mark_argnames(mark)
+ for mark in metafunc.definition.iter_markers("parametrize")
+ ):
+ continue
+
+ # In the common case we only look at the fixture def with the
+ # closest scope (last in the list). But if the fixture overrides
+ # another fixture, while requesting the super fixture, keep going
+ # in case the super fixture is parametrized (#1953).
+ for fixturedef in reversed(fixture_defs):
+ # Fixture is parametrized, apply it and stop.
+ if fixturedef.params is not None:
+ metafunc.parametrize(
+ argname,
+ fixturedef.params,
+ indirect=True,
+ scope=fixturedef.scope,
+ ids=fixturedef.ids,
+ )
+ break
+
+ # Not requesting the overridden super fixture, stop.
+ #
+ # TODO: Handle the case where the super-fixture is transitively
+ # requested (see #7737 and the xfail'd test
+ # test_override_parametrized_fixture_via_transitive_fixture).
+ if argname not in fixturedef.argnames:
+ break
+
+ # Try next super fixture, if any.
+
+ def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> None:
+ # Separate parametrized setups.
+ items[:] = reorder_items(items)
+
+ def _register_fixture(
+ self,
+ *,
+ name: str,
+ func: _FixtureFunc[object],
+ nodeid: str | None | NotSetType = NOTSET,
+ scope: Scope | ScopeName | Callable[[str, Config], ScopeName] = "function",
+ params: Sequence[object] | None = None,
+ ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
+ autouse: bool = False,
+ node: nodes.Node | NotSetType = NOTSET,
+ ) -> None:
+ """Register a fixture
+
+ :param name:
+ The fixture's name.
+ :param func:
+ The fixture's implementation function.
+ :param nodeid:
+ The visibility of the fixture (deprecated, use node instead).
+ The fixture will be available to the node with this nodeid and
+ its children in the collection tree. None means global visibility.
+ :param node:
+ The node where the fixture is defined (preferred over nodeid).
+ When provided, enables node-based matching which is more robust.
+ :param scope:
+ The fixture's scope.
+ :param params:
+ The fixture's parametrization params.
+ :param ids:
+ The fixture's IDs.
+ :param autouse:
+ Whether this is an autouse fixture.
+ """
+ # Emit deprecation warning if nodeid string.
+ if nodeid is not NOTSET or node is NOTSET:
+ warnings.warn(FIXTURE_NODEID_DEPRECATED, stacklevel=2)
+ fixture_def = FixtureDef(
+ config=self.config,
+ baseid=nodeid,
+ argname=name,
+ func=func,
+ scope=scope,
+ params=params,
+ ids=ids,
+ _ispytest=True,
+ _autouse=autouse,
+ node=node,
+ )
+
+ faclist = self._arg2fixturedefs.setdefault(name, [])
+ # Insert the fixturedef into the list while maintaining a partial order
+ # based on visibility: a fixturedef whose visibility is more specific
+ # sorts after a more general one, so that it takes precedence in the
+ # override chain (the last applicable fixturedef in the list is used
+ # first, see getfixturedefs).
+ # fixturedefs with the same visibility keep registration order, i.e. the
+ # last registered wins.
+ # The order between non-comparable fixturedefs doesn't matter since they
+ # cannot be visible together.
+ # The idea is that a fixture that is defined closer to the item should
+ # take precedence.
+ for i, existing in enumerate(faclist):
+ if is_visibility_more_specific(existing, fixture_def):
+ faclist.insert(i, fixture_def)
+ break
+ else:
+ faclist.append(fixture_def)
+ if autouse:
+ if node is not NOTSET:
+ self._node_autousenames.setdefault(node, []).append(name)
+ elif nodeid is not NOTSET and nodeid is not None:
+ # Legacy: plugin passed nodeid string without node reference.
+ self._nodeid_autousenames.setdefault(nodeid, []).append(name)
+ else:
+ # Global plugin autouse fixtures go under Session.
+ self._node_autousenames.setdefault(self.session, []).append(name)
+
+ @overload
+ def parsefactories(
+ self,
+ node_or_obj: nodes.Node,
+ ) -> None:
+ raise NotImplementedError()
+
+ @overload
+ @deprecated(
+ "parsefactories(obj, nodeid) is deprecated, use parsefactories(holder=obj, node=node) instead"
+ )
+ def parsefactories(
+ self,
+ node_or_obj: object,
+ nodeid: str | None,
+ ) -> None:
+ raise NotImplementedError()
+
+ @overload
+ def parsefactories(
+ self,
+ node_or_obj: NotSetType = ...,
+ nodeid: NotSetType = ...,
+ *,
+ holder: object,
+ node: nodes.Node,
+ ) -> None:
+ raise NotImplementedError()
+
+ def parsefactories(
+ self,
+ node_or_obj: nodes.Node | object | NotSetType = NOTSET,
+ nodeid: str | None | NotSetType = NOTSET,
+ *,
+ holder: object | NotSetType = NOTSET,
+ node: nodes.Node | NotSetType = NOTSET,
+ ) -> None:
+ """Collect fixtures from a collection node or object.
+
+ Found fixtures are parsed into `FixtureDef`s and saved.
+
+ The preferred API uses keyword-only arguments:
+ - ``holder``: The object to scan for fixtures.
+ - ``node``: The node determining fixture visibility.
+
+ Legacy positional API (translated internally):
+ - ``parsefactories(node)``: Uses node.obj as holder, node for scope.
+ - ``parsefactories(obj, nodeid)``: Uses obj as holder, nodeid string for scope.
+ """
+ # Translate legacy API to holder/node sources of truth
+ # Either effective_node or effective_nodeid will be set, not both
+ effective_node: nodes.Node | NotSetType = NOTSET
+ effective_nodeid: str | None | NotSetType = NOTSET
+
+ if holder is not NOTSET:
+ # New API: holder and node explicitly provided
+ holderobj = holder
+ effective_node = node
+ elif node_or_obj is NOTSET:
+ raise TypeError("parsefactories() requires holder or node_or_obj")
+ elif nodeid is not NOTSET:
+ # Legacy: parsefactories(obj, nodeid) - string-based scoping only.
+ warnings.warn(PARSEFACTORIES_NODEID_DEPRECATED, stacklevel=2)
+ holderobj = node_or_obj
+ effective_nodeid = nodeid
+ else:
+ # parsefactories(node) - node has .obj attribute
+ assert isinstance(node_or_obj, nodes.Node)
+ holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
+ effective_node = node_or_obj
+
+ # Avoid accessing `@property` (and other descriptors) when iterating fixtures.
+ if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType):
+ holderobj_tp: object = type(holderobj)
+ else:
+ holderobj_tp = holderobj
+
+ for name in dir(holderobj):
+ # The attribute can be an arbitrary descriptor, so the attribute
+ # access below can raise. safe_getattr() ignores such exceptions.
+ obj_ub = safe_getattr(holderobj_tp, name, None)
+ if type(obj_ub) is FixtureFunctionDefinition:
+ marker = obj_ub._fixture_function_marker
+ if marker.name:
+ fixture_name = marker.name
+ else:
+ fixture_name = name
+
+ # OK we know it is a fixture -- now safe to look up on the _instance_.
+ try:
+ obj = getattr(holderobj, name)
+ # if the fixture is named in the decorator we cannot find it in the module
+ except AttributeError:
+ obj = obj_ub
+
+ func = obj._get_wrapped_function()
+
+ self._register_fixture(
+ name=fixture_name,
+ func=func,
+ scope=marker.scope,
+ params=marker.params,
+ ids=marker.ids,
+ autouse=marker.autouse,
+ node=effective_node,
+ nodeid=effective_nodeid,
+ )
+
+ def getfixturedefs(
+ self, argname: str, node: nodes.Node
+ ) -> Sequence[FixtureDef[Any]] | None:
+ """Get FixtureDefs for a fixture name which are applicable
+ to a given node.
+
+ Returns None if there are no fixtures at all defined with the given
+ name. (This is different from the case in which there are fixtures
+ with the given name, but none applicable to the node. In this case,
+ an empty result is returned).
+
+ :param argname: Name of the fixture to search for.
+ :param node: The requesting Node.
+ """
+ try:
+ fixturedefs = self._arg2fixturedefs[argname]
+ except KeyError:
+ return None
+ return tuple(self._matchfactories(fixturedefs, node))
+
+ def _matchfactories(
+ self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
+ ) -> Iterator[FixtureDef[Any]]:
+ # Collect parent nodes and their IDs for matching
+ parent_nodes = set(node.iter_parents())
+ parentnodeids = {n.nodeid for n in parent_nodes}
+
+ for fixturedef in fixturedefs:
+ if fixturedef.node is not None:
+ # Node-based matching: check if fixture's node is a parent
+ if fixturedef.node in parent_nodes:
+ yield fixturedef
+ elif fixturedef.baseid in parentnodeids:
+ # Fallback to string-based matching for legacy/plugins
+ yield fixturedef
+
+
+def show_fixtures_per_test(config: Config) -> int | ExitCode:
+ from _pytest.main import wrap_session
+
+ return wrap_session(config, _show_fixtures_per_test)
+
+
+_PYTEST_DIR = Path(_pytest.__file__).parent
+
+
+def _pretty_fixture_path(invocation_dir: Path, func) -> str:
+ loc = Path(getlocation(func, invocation_dir))
+ prefix = Path("...", "_pytest")
+ try:
+ return str(prefix / loc.relative_to(_PYTEST_DIR))
+ except ValueError:
+ return bestrelpath(invocation_dir, loc)
+
+
+def _get_fixtures_per_test(test: nodes.Item) -> Iterator[FixtureDef[object]]:
+ """Returns all fixtures used by the test item except for those created by
+ direct parametrization and those requested dynamically with
+ ``request.getfixturevalue``.
+
+ The justification for excluding fixtures created by direct parametrization
+ is that for users, they are internal implementation detail.
+
+ Dynamically requested fixtures are excluded because they are not known
+ statically.
+ """
+ from _pytest.python import DirectParamFixtureDef
+
+ # Custom Items may not have _fixtureinfo attribute.
+ fixture_info: FuncFixtureInfo | None = getattr(test, "_fixtureinfo", None)
+ if fixture_info is None:
+ return # pragma: no cover
+
+ # dict key not used in loop but needed for sorting.
+ for argname, fixturedefs in sorted(fixture_info.name2fixturedefs.items()):
+ if not fixturedefs:
+ # Not supposed to be empty, but for safety.
+ continue # pragma: no cover
+ # Last item is expected to be the one directly used by the test item.
+ fixturedef = fixturedefs[-1]
+ if isinstance(fixturedef, DirectParamFixtureDef):
+ continue
+ yield fixturedef
+
+
+def _show_fixtures_per_test(config: Config, session: Session) -> None:
+ import _pytest.config
+
+ session.perform_collect()
+ invocation_dir = config.invocation_params.dir
+ tw = _pytest.config.create_terminal_writer(config)
+ verbose = config.get_verbosity()
+
+ def get_best_relpath(func) -> str:
+ loc = getlocation(func, invocation_dir)
+ return bestrelpath(invocation_dir, Path(loc))
+
+ def write_fixture(fixture_def: FixtureDef[object]) -> None:
+ argname = fixture_def.argname
+ if verbose <= 0 and argname.startswith("_"):
+ return
+ prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
+ tw.write(f"{argname}", green=True)
+ tw.write(f" -- {prettypath}", yellow=True)
+ tw.write("\n")
+ fixture_doc = inspect.getdoc(fixture_def.func)
+ if fixture_doc:
+ write_docstring(
+ tw,
+ fixture_doc.split("\n\n", maxsplit=1)[0]
+ if verbose <= 0
+ else fixture_doc,
+ )
+ else:
+ tw.line(" no docstring available", red=True)
+
+ def write_item(item: nodes.Item) -> None:
+ fixturedefs = list(_get_fixtures_per_test(item))
+ if not fixturedefs:
+ # This test item does not use any fixtures.
+ return
+
+ tw.line()
+ tw.sep("-", f"fixtures used by {item.name}")
+ # TODO: Fix this type ignore.
+ tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
+
+ for fixturedef in fixturedefs:
+ write_fixture(fixturedef)
+
+ for session_item in session.items:
+ write_item(session_item)
+
+
+def showfixtures(config: Config) -> int | ExitCode:
+ from _pytest.main import wrap_session
+
+ return wrap_session(config, _showfixtures_main)
+
+
+def _showfixtures_main(config: Config, session: Session) -> None:
+ import _pytest.config
+
+ session.perform_collect()
+ invocation_dir = config.invocation_params.dir
+ tw = _pytest.config.create_terminal_writer(config)
+ verbose = config.get_verbosity()
+
+ fm = session._fixturemanager
+
+ available = []
+ seen: set[tuple[str, str]] = set()
+
+ for argname, fixturedefs in fm._arg2fixturedefs.items():
+ assert fixturedefs is not None
+ if not fixturedefs:
+ continue
+ for fixturedef in fixturedefs:
+ loc = getlocation(fixturedef.func, invocation_dir)
+ if (fixturedef.argname, loc) in seen:
+ continue
+ seen.add((fixturedef.argname, loc))
+ available.append(
+ (
+ len(fixturedef.baseid),
+ fixturedef.func.__module__,
+ _pretty_fixture_path(invocation_dir, fixturedef.func),
+ fixturedef.argname,
+ fixturedef,
+ )
+ )
+
+ available.sort()
+ currentmodule = None
+ for baseid, module, prettypath, argname, fixturedef in available:
+ if currentmodule != module:
+ if not module.startswith("_pytest."):
+ tw.line()
+ tw.sep("-", f"fixtures defined from {module}")
+ currentmodule = module
+ if verbose <= 0 and argname.startswith("_"):
+ continue
+ tw.write(f"{argname}", green=True)
+ if fixturedef.scope != "function":
+ tw.write(f" [{fixturedef.scope} scope]", cyan=True)
+ tw.write(f" -- {prettypath}", yellow=True)
+ tw.write("\n")
+ doc = inspect.getdoc(fixturedef.func)
+ if doc:
+ write_docstring(
+ tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc
+ )
+ else:
+ tw.line(" no docstring available", red=True)
+ tw.line()
+
+
+def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
+ for line in doc.split("\n"):
+ tw.line(indent + line)
+
+
+def register_fixture(
+ *,
+ name: str,
+ func: _FixtureFunc[object],
+ node: nodes.Node,
+ scope: ScopeName | Callable[[str, Config], ScopeName] = "function",
+ params: Sequence[object] | None = None,
+ ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
+ autouse: bool = False,
+) -> None:
+ """Register a fixture imperatively.
+
+ This is an advanced function intended for use by plugins.
+
+ Normally, fixtures should be registered declaratively using the
+ :func:`@pytest.fixture ` decorator. Pytest looks for these
+ fixture definitions during the collection phase and registers them
+ automatically. For some plugin usecases the declarative interface can be
+ cumbersome or nonviable, in which case the imperative interface can be used.
+
+ Fixture registration is expected to happen during the collection phase, and
+ this is the only sanctioned use. However, to allow for more creative uses,
+ this is not enforced. But do so at your own risk!
+
+ .. versionadded: 9.1
+
+ :param name:
+ The fixture's name.
+ :param func:
+ The fixture's implementation function.
+ :param node:
+ The visibility of the fixture.
+
+ Only items that are descendents of this node in the collection tree will
+ be able to request this fixture. You can think of this as the place
+ where you would put the `@pytest.fixture`.
+
+ For global visibility, pass the :class:`session ` node,
+ which is the root of the collection tree.
+ :param scope:
+ The fixture's scope.
+ :param params:
+ The fixture's parametrization params.
+ :param ids:
+ The fixture's IDs.
+ :param autouse:
+ Whether this is an autouse fixture.
+ """
+ node.session._fixturemanager._register_fixture(
+ name=name,
+ func=func,
+ node=node,
+ scope=scope,
+ params=params,
+ ids=ids,
+ autouse=autouse,
+ )
diff --git a/venv/lib/python3.11/site-packages/_pytest/freeze_support.py b/venv/lib/python3.11/site-packages/_pytest/freeze_support.py
new file mode 100644
index 000000000..959ff071d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/freeze_support.py
@@ -0,0 +1,45 @@
+"""Provides a function to report all internal modules for using freezing
+tools."""
+
+from __future__ import annotations
+
+from collections.abc import Iterator
+import types
+
+
+def freeze_includes() -> list[str]:
+ """Return a list of module names used by pytest that should be
+ included by cx_freeze."""
+ import _pytest
+
+ result = list(_iter_all_modules(_pytest))
+ return result
+
+
+def _iter_all_modules(
+ package: str | types.ModuleType,
+ prefix: str = "",
+) -> Iterator[str]:
+ """Iterate over the names of all modules that can be found in the given
+ package, recursively.
+
+ >>> import _pytest
+ >>> list(_iter_all_modules(_pytest))
+ ['_pytest._argcomplete', '_pytest._code.code', ...]
+ """
+ import os
+ import pkgutil
+
+ if isinstance(package, str):
+ path = package
+ else:
+ # Type ignored because typeshed doesn't define ModuleType.__path__
+ # (only defined on packages).
+ package_path = package.__path__
+ path, prefix = package_path[0], package.__name__ + "."
+ for _, name, is_package in pkgutil.iter_modules([path]):
+ if is_package:
+ for m in _iter_all_modules(os.path.join(path, name), prefix=name + "."):
+ yield prefix + m
+ else:
+ yield prefix + name
diff --git a/venv/lib/python3.11/site-packages/_pytest/helpconfig.py b/venv/lib/python3.11/site-packages/_pytest/helpconfig.py
new file mode 100644
index 000000000..fdba02b35
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/helpconfig.py
@@ -0,0 +1,293 @@
+# mypy: allow-untyped-defs
+"""Version info, help messages, tracing configuration."""
+
+from __future__ import annotations
+
+import argparse
+from collections.abc import Generator
+from collections.abc import Sequence
+import os
+import sys
+from typing import Any
+
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import PrintHelp
+from _pytest.config.argparsing import Parser
+from _pytest.terminal import TerminalReporter
+import pytest
+
+
+class HelpAction(argparse.Action):
+ """An argparse Action that will raise a PrintHelp exception in order to skip
+ the rest of the argument parsing when --help is passed.
+
+ This prevents argparse from raising UsageError when `--help` is used along
+ with missing required arguments when any are defined, for example by
+ ``pytest_addoption``. This is similar to the way that the builtin argparse
+ --help option is implemented by raising SystemExit.
+
+ To opt in to this behavior, the parse caller must set
+ `namespace._raise_print_help = True`. Otherwise it just sets the option.
+ """
+
+ def __init__(
+ self, option_strings: Sequence[str], dest: str, *, help: str | None = None
+ ) -> None:
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=0,
+ const=True,
+ default=False,
+ help=help,
+ )
+
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: str | Sequence[Any] | None,
+ option_string: str | None = None,
+ ) -> None:
+ setattr(namespace, self.dest, self.const)
+
+ if getattr(namespace, "_raise_print_help", False):
+ raise PrintHelp
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "-V",
+ "--version",
+ action="count",
+ default=0,
+ dest="version",
+ help="Display pytest version and information about plugins. "
+ "When given twice, also display information about plugins.",
+ )
+ group._addoption( # private to use reserved lower-case short option
+ "-h",
+ "--help",
+ action=HelpAction,
+ dest="help",
+ help="Show help message and configuration info",
+ )
+ group._addoption( # private to use reserved lower-case short option
+ "-p",
+ action="append",
+ dest="plugins",
+ default=[],
+ metavar="name",
+ help="Early-load given plugin module name or entry point (multi-allowed). "
+ "To avoid loading of plugins, use the `no:` prefix, e.g. "
+ "`no:doctest`. See also --disable-plugin-autoload.",
+ )
+ group.addoption(
+ "--disable-plugin-autoload",
+ action="store_true",
+ default=False,
+ help="Disable plugin auto-loading through entry point packaging metadata. "
+ "Only plugins explicitly specified in -p or env var PYTEST_PLUGINS will be loaded.",
+ )
+ group.addoption(
+ "--traceconfig",
+ "--trace-config",
+ action="store_true",
+ default=False,
+ help="Trace considerations of conftest.py files",
+ )
+ group.addoption(
+ "--debug",
+ action="store",
+ nargs="?",
+ const="pytestdebug.log",
+ dest="debug",
+ metavar="DEBUG_FILE_NAME",
+ help="Store internal tracing debug information in this log file. "
+ "This file is opened with 'w' and truncated as a result, care advised. "
+ "Default: pytestdebug.log.",
+ )
+ group._addoption( # private to use reserved lower-case short option
+ "-o",
+ "--override-ini",
+ dest="override_ini",
+ action="append",
+ help='Override configuration option with "option=value" style, '
+ "e.g. `-o strict_xfail=True -o cache_dir=cache`.",
+ )
+
+
+@pytest.hookimpl(wrapper=True)
+def pytest_cmdline_parse() -> Generator[None, Config, Config]:
+ config = yield
+
+ if config.option.debug:
+ # --debug | --debug was provided.
+ path = config.option.debug
+ debugfile = open(path, "w", encoding="utf-8")
+ debugfile.write(
+ "versions pytest-{}, "
+ "python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
+ pytest.__version__,
+ ".".join(map(str, sys.version_info)),
+ config.invocation_params.dir,
+ os.getcwd(),
+ config.invocation_params.args,
+ )
+ )
+ config.trace.root.setwriter(debugfile.write)
+ undo_tracing = config.pluginmanager.enable_tracing()
+ sys.stderr.write(f"writing pytest debug information to {path}\n")
+
+ def unset_tracing() -> None:
+ debugfile.close()
+ sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n")
+ config.trace.root.setwriter(None)
+ undo_tracing()
+
+ config.add_cleanup(unset_tracing)
+
+ return config
+
+
+def show_version_verbose(config: Config) -> None:
+ """Show verbose pytest version installation, including plugins."""
+ sys.stdout.write(
+ f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
+ )
+ plugininfo = getpluginversioninfo(config)
+ if plugininfo:
+ for line in plugininfo:
+ sys.stdout.write(line + "\n")
+
+
+def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
+ # Note: a single `--version` argument is handled directly by `Config.main()` to avoid starting up the entire
+ # pytest infrastructure just to display the version (#13574).
+ if config.option.version > 1:
+ show_version_verbose(config)
+ return ExitCode.OK
+ elif config.option.help:
+ config._do_configure()
+ showhelp(config)
+ config._ensure_unconfigure()
+ return ExitCode.OK
+ return None
+
+
+def showhelp(config: Config) -> None:
+ import textwrap
+
+ reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
+ "terminalreporter"
+ )
+ assert reporter is not None
+ tw = reporter._tw
+ tw.write(config._parser.optparser.format_help())
+ tw.line()
+ tw.line(
+ "[pytest] configuration options in the first "
+ "pytest.toml|pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
+ )
+ tw.line()
+
+ columns = tw.fullwidth # costly call
+ indent_len = 24 # based on argparse's max_help_position=24
+ indent = " " * indent_len
+ for name in config._parser._inidict:
+ help, type, _default = config._parser._inidict[name]
+ if help is None:
+ raise TypeError(f"help argument cannot be None for {name}")
+ spec = f"{name} ({type}):"
+ tw.write(f" {spec}")
+ spec_len = len(spec)
+ if spec_len > (indent_len - 3):
+ # Display help starting at a new line.
+ tw.line()
+ helplines = textwrap.wrap(
+ help,
+ columns,
+ initial_indent=indent,
+ subsequent_indent=indent,
+ break_on_hyphens=False,
+ )
+
+ for line in helplines:
+ tw.line(line)
+ else:
+ # Display help starting after the spec, following lines indented.
+ tw.write(" " * (indent_len - spec_len - 2))
+ wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False)
+
+ if wrapped:
+ tw.line(wrapped[0])
+ for line in wrapped[1:]:
+ tw.line(indent + line)
+
+ tw.line()
+ tw.line("Environment variables:")
+ vars = [
+ (
+ "CI",
+ "When set to a non-empty value, pytest knows it is running in a "
+ "CI process and does not truncate summary info",
+ ),
+ ("BUILD_NUMBER", "Equivalent to CI"),
+ ("PYTEST_ADDOPTS", "Extra command line options"),
+ ("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"),
+ ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"),
+ ("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"),
+ ("PYTEST_DEBUG_TEMPROOT", "Override the system temporary directory"),
+ ("PYTEST_THEME", "The Pygments style to use for code output"),
+ ("PYTEST_THEME_MODE", "Set the PYTEST_THEME to be either 'dark' or 'light'"),
+ ]
+ for name, help in vars:
+ tw.line(f" {name:<24} {help}")
+ tw.line()
+ tw.line()
+
+ tw.line("to see available markers type: pytest --markers")
+ tw.line("to see available fixtures type: pytest --fixtures")
+ tw.line(
+ "(shown according to specified file_or_dir or current dir "
+ "if not specified; fixtures with leading '_' are only shown "
+ "with the '-v' option"
+ )
+
+ for warningreport in reporter.stats.get("warnings", []):
+ tw.line("warning : " + warningreport.message, red=True)
+
+
+def getpluginversioninfo(config: Config) -> list[str]:
+ lines = []
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
+ if plugininfo:
+ lines.append("registered third-party plugins:")
+ for plugin, dist in plugininfo:
+ loc = getattr(plugin, "__file__", repr(plugin))
+ content = f"{dist.project_name}-{dist.version} at {loc}"
+ lines.append(" " + content)
+ return lines
+
+
+def pytest_report_header(config: Config) -> list[str]:
+ lines = []
+ if config.option.debug or config.option.traceconfig:
+ lines.append(f"using: pytest-{pytest.__version__}")
+
+ verinfo = getpluginversioninfo(config)
+ if verinfo:
+ lines.extend(verinfo)
+
+ if config.option.traceconfig:
+ lines.append("active plugins:")
+ items = config.pluginmanager.list_name_plugin()
+ for name, plugin in items:
+ if hasattr(plugin, "__file__"):
+ r = plugin.__file__
+ else:
+ r = repr(plugin)
+ lines.append(f" {name:<20}: {r}")
+ return lines
diff --git a/venv/lib/python3.11/site-packages/_pytest/hookspec.py b/venv/lib/python3.11/site-packages/_pytest/hookspec.py
new file mode 100644
index 000000000..6c5dd4b7c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/hookspec.py
@@ -0,0 +1,1289 @@
+# mypy: allow-untyped-defs
+# ruff: noqa: T100
+"""Hook specifications for pytest plugins which are invoked by pytest itself
+and by builtin plugins."""
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from collections.abc import Sequence
+from pathlib import Path
+from typing import Any
+from typing import TYPE_CHECKING
+
+from pluggy import HookspecMarker
+
+
+if TYPE_CHECKING:
+ import pdb
+ from typing import Literal
+ import warnings
+
+ from _pytest._code.code import ExceptionInfo
+ from _pytest._code.code import ExceptionRepr
+ from _pytest.config import _PluggyPlugin
+ from _pytest.config import Config
+ from _pytest.config import ExitCode
+ from _pytest.config import PytestPluginManager
+ from _pytest.config.argparsing import Parser
+ from _pytest.fixtures import FixtureDef
+ from _pytest.fixtures import SubRequest
+ from _pytest.main import Session
+ from _pytest.nodes import Collector
+ from _pytest.nodes import Item
+ from _pytest.outcomes import Exit
+ from _pytest.python import Class
+ from _pytest.python import Function
+ from _pytest.python import Metafunc
+ from _pytest.python import Module
+ from _pytest.reports import CollectReport
+ from _pytest.reports import TestReport
+ from _pytest.runner import CallInfo
+ from _pytest.terminal import TerminalReporter
+ from _pytest.terminal import TestShortLogReport
+
+
+hookspec = HookspecMarker("pytest")
+
+# -------------------------------------------------------------------------
+# Initialization hooks called for every plugin
+# -------------------------------------------------------------------------
+
+
+@hookspec(historic=True)
+def pytest_addhooks(pluginmanager: PytestPluginManager) -> None:
+ """Called at plugin registration time to allow adding new hooks via a call to
+ :func:`pluginmanager.add_hookspecs(module_or_class, prefix) `.
+
+ :param pluginmanager: The pytest plugin manager.
+
+ .. note::
+ This hook is incompatible with hook wrappers.
+
+ Use in conftest plugins
+ =======================
+
+ If a conftest plugin implements this hook, it will be called immediately
+ when the conftest is registered.
+ """
+
+
+@hookspec(historic=True)
+def pytest_plugin_registered(
+ plugin: _PluggyPlugin,
+ plugin_name: str,
+ manager: PytestPluginManager,
+) -> None:
+ """A new pytest plugin got registered.
+
+ :param plugin: The plugin module or instance.
+ :param plugin_name: The name by which the plugin is registered.
+ :param manager: The pytest plugin manager.
+
+ .. note::
+ This hook is incompatible with hook wrappers.
+
+ Use in conftest plugins
+ =======================
+
+ If a conftest plugin implements this hook, it will be called immediately
+ when the conftest is registered, once for each plugin registered thus far
+ (including itself!), and for all plugins thereafter when they are
+ registered.
+ """
+
+
+@hookspec(historic=True)
+def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None:
+ """Register argparse-style options and config-style config values,
+ called once at the beginning of a test run.
+
+ :param parser:
+ To add command line options, call
+ :py:func:`parser.addoption(...) `.
+ To add config-file values call :py:func:`parser.addini(...)
+ `.
+
+ :param pluginmanager:
+ The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
+ or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
+ to change how command line options are added.
+
+ Options can later be accessed through the
+ :py:class:`config ` object, respectively:
+
+ - :py:func:`config.getoption(name) ` to
+ retrieve the value of a command line option.
+
+ - :py:func:`config.getini(name) ` to retrieve
+ a value read from a configuration file.
+
+ The config object is passed around on many internal objects via the ``.config``
+ attribute or can be retrieved as the ``pytestconfig`` fixture.
+
+ .. note::
+ This hook is incompatible with hook wrappers.
+
+ Use in conftest plugins
+ =======================
+
+ If a conftest plugin implements this hook, it will be called immediately
+ when the conftest is registered.
+
+ This hook is only called for :ref:`initial conftests `.
+ """
+
+
+@hookspec(historic=True)
+def pytest_configure(config: Config) -> None:
+ """Allow plugins and conftest files to perform initial configuration.
+
+ .. note::
+ This hook is incompatible with hook wrappers.
+
+ :param config: The pytest config object.
+
+ Use in conftest plugins
+ =======================
+
+ This hook is called for every :ref:`initial conftest ` file
+ after command line options have been parsed. After that, the hook is called
+ for other conftest files as they are registered.
+ """
+
+
+# -------------------------------------------------------------------------
+# Bootstrapping hooks called for plugins registered early enough:
+# internal and 3rd party plugins.
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_cmdline_parse(
+ pluginmanager: PytestPluginManager, args: list[str]
+) -> Config | None:
+ """Return an initialized :class:`~pytest.Config`, parsing the specified args.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ .. note::
+ This hook is only called for plugin classes passed to the
+ ``plugins`` arg when using `pytest.main`_ to perform an in-process
+ test run.
+
+ :param pluginmanager: The pytest plugin manager.
+ :param args: List of arguments passed on the command line.
+ :returns: A pytest config object.
+
+ Use in conftest plugins
+ =======================
+
+ This hook is not called for conftest files.
+ """
+
+
+def pytest_load_initial_conftests(
+ early_config: Config, parser: Parser, args: list[str]
+) -> None:
+ """Called to implement the loading of :ref:`initial conftest files
+ ` ahead of command line option parsing.
+
+ :param early_config: The pytest config object.
+ :param args: Arguments passed on the command line.
+ :param parser: To add command line options.
+
+ Use in conftest plugins
+ =======================
+
+ This hook is not called for conftest files.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_cmdline_main(config: Config) -> ExitCode | int | None:
+ """Called for performing the main command line action.
+
+ The default implementation will invoke the configure hooks and
+ :hook:`pytest_runtestloop`.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param config: The pytest config object.
+ :returns: The exit code.
+
+ Use in conftest plugins
+ =======================
+
+ This hook is only called for :ref:`initial conftests `.
+ """
+
+
+# -------------------------------------------------------------------------
+# collection hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_collection(session: Session) -> object | None:
+ """Perform the collection phase for the given session.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+
+ The default collection phase is this (see individual hooks for full details):
+
+ 1. Starting from ``session`` as the initial collector:
+
+ 1. ``pytest_collectstart(collector)``
+ 2. ``report = pytest_make_collect_report(collector)``
+ 3. ``pytest_exception_interact(collector, call, report)`` if an interactive exception occurred
+ 4. For each collected node:
+
+ 1. If an item, ``pytest_itemcollected(item)``
+ 2. If a collector, recurse into it.
+
+ 5. ``pytest_collectreport(report)``
+
+ 2. ``pytest_collection_modifyitems(session, config, items)``
+
+ 1. ``pytest_deselected(items)`` for any deselected items (may be called multiple times)
+
+ 3. Set ``session.items`` to the list of collected items
+ 4. ``pytest_collection_finish(session)``
+ 5. Set ``session.testscollected`` to the number of collected items
+
+ You can implement this hook to only perform some action before collection,
+ for example the terminal plugin uses it to start displaying the collection
+ counter (and returns `None`).
+
+ :param session: The pytest session object.
+
+ Use in conftest plugins
+ =======================
+
+ This hook is only called for :ref:`initial conftests `.
+ """
+
+
+def pytest_collection_modifyitems(
+ session: Session, config: Config, items: list[Item]
+) -> None:
+ """Called after collection has been performed. May filter or re-order
+ the items in-place.
+
+ When items are deselected (filtered out from ``items``),
+ the hook :hook:`pytest_deselected` must be called explicitly
+ with the deselected items to properly notify other plugins,
+ e.g. with ``config.hook.pytest_deselected(items=deselected_items)``.
+
+ :param session: The pytest session object.
+ :param config: The pytest config object.
+ :param items: List of item objects.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+def pytest_collection_finish(session: Session) -> None:
+ """Called after collection has been performed and modified.
+
+ :param session: The pytest session object.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:
+ """Return ``True`` to ignore this path for collection.
+
+ Return ``None`` to let other plugins ignore the path for collection.
+
+ Returning ``False`` will forcefully *not* ignore this path for collection,
+ without giving a chance for other plugins to ignore this path.
+
+ This hook is consulted for all files and directories prior to calling
+ more specific hooks.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param collection_path: The path to analyze.
+ :type collection_path: pathlib.Path
+ :param config: The pytest config object.
+
+ .. versionchanged:: 7.0.0
+ The ``collection_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``path`` parameter. The ``path`` parameter
+ has been deprecated and removed in pytest 9.0.0.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given collection path, only
+ conftest files in parent directories of the collection path are consulted
+ (if the path is a directory, its own conftest file is *not* consulted - a
+ directory cannot ignore itself!).
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_collect_directory(path: Path, parent: Collector) -> Collector | None:
+ """Create a :class:`~pytest.Collector` for the given directory, or None if
+ not relevant.
+
+ .. versionadded:: 8.0
+
+ For best results, the returned collector should be a subclass of
+ :class:`~pytest.Directory`, but this is not required.
+
+ The new node needs to have the specified ``parent`` as a parent.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param path: The path to analyze.
+ :type path: pathlib.Path
+
+ See :ref:`custom directory collectors` for a simple example of use of this
+ hook.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given collection path, only
+ conftest files in parent directories of the collection path are consulted
+ (if the path is a directory, its own conftest file is *not* consulted - a
+ directory cannot collect itself!).
+ """
+
+
+def pytest_collect_file(file_path: Path, parent: Collector) -> Collector | None:
+ """Create a :class:`~pytest.Collector` for the given path, or None if not relevant.
+
+ For best results, the returned collector should be a subclass of
+ :class:`~pytest.File`, but this is not required.
+
+ The new node needs to have the specified ``parent`` as a parent.
+
+ :param file_path: The path to analyze.
+ :type file_path: pathlib.Path
+
+ .. versionchanged:: 7.0.0
+ The ``file_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``path`` parameter. The ``path`` parameter
+ has been deprecated and removed in pytest 9.0.0.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given file path, only
+ conftest files in parent directories of the file path are consulted.
+ """
+
+
+# logging hooks for collection
+
+
+def pytest_collectstart(collector: Collector) -> None:
+ """Collector starts collecting.
+
+ :param collector:
+ The collector.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given collector, only
+ conftest files in the collector's directory and its parent directories are
+ consulted.
+ """
+
+
+def pytest_itemcollected(item: Item) -> None:
+ """We just collected a test item.
+
+ :param item:
+ The item.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+def pytest_collectreport(report: CollectReport) -> None:
+ """Collector finished collecting.
+
+ :param report:
+ The collect report.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given collector, only
+ conftest files in the collector's directory and its parent directories are
+ consulted.
+ """
+
+
+def pytest_deselected(items: Sequence[Item]) -> None:
+ """Called for deselected test items, e.g. by keyword.
+
+ Note that this hook has two integration aspects for plugins:
+
+ - it can be *implemented* to be notified of deselected items
+ - it must be *called* from :hook:`pytest_collection_modifyitems`
+ implementations when items are deselected (to properly notify other plugins).
+
+ May be called multiple times.
+
+ :param items:
+ The items.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_make_collect_report(collector: Collector) -> CollectReport | None:
+ """Perform :func:`collector.collect() ` and return
+ a :class:`~pytest.CollectReport`.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param collector:
+ The collector.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given collector, only
+ conftest files in the collector's directory and its parent directories are
+ consulted.
+ """
+
+
+# -------------------------------------------------------------------------
+# Python test function related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_pycollect_makemodule(module_path: Path, parent) -> Module | None:
+ """Return a :class:`pytest.Module` collector or None for the given path.
+
+ This hook will be called for each matching test module path.
+ The :hook:`pytest_collect_file` hook needs to be used if you want to
+ create test modules for files that do not match as a test module.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param module_path: The path of the module to collect.
+ :type module_path: pathlib.Path
+
+ .. versionchanged:: 7.0.0
+ The ``module_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``path`` parameter. The ``path`` parameter has been
+ deprecated in favor of ``module_path`` and removed in pytest 9.0.0.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given parent collector,
+ only conftest files in the collector's directory and its parent directories
+ are consulted.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_pycollect_makeitem(
+ collector: Module | Class, name: str, obj: object
+) -> None | Item | Collector | list[Item | Collector]:
+ """Return a custom item/collector for a Python object in a module, or None.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param collector:
+ The module/class collector.
+ :param name:
+ The name of the object in the module/class.
+ :param obj:
+ The object.
+ :returns:
+ The created items/collectors.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given collector, only
+ conftest files in the collector's directory and its parent directories
+ are consulted.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
+ """Call underlying test function.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param pyfuncitem:
+ The function item.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only
+ conftest files in the item's directory and its parent directories
+ are consulted.
+ """
+
+
+def pytest_generate_tests(metafunc: Metafunc) -> None:
+ """Generate (multiple) parametrized calls to a test function.
+
+ :param metafunc:
+ The :class:`~pytest.Metafunc` helper for the test function.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given function definition,
+ only conftest files in the functions's directory and its parent directories
+ are consulted.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_make_parametrize_id(config: Config, val: object, argname: str) -> str | None:
+ """Return a user-friendly string representation of the given ``val``
+ that will be used by @pytest.mark.parametrize calls, or None if the hook
+ doesn't know about ``val``.
+
+ The parameter name is available as ``argname``, if required.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ :param config: The pytest config object.
+ :param val: The parametrized value.
+ :param argname: The automatic parameter name produced by pytest.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook.
+ """
+
+
+# -------------------------------------------------------------------------
+# runtest related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_runtestloop(session: Session) -> object | None:
+ """Perform the main runtest loop (after collection finished).
+
+ The default hook implementation performs the runtest protocol for all items
+ collected in the session (``session.items``), unless the collection failed
+ or the ``collectonly`` pytest option is set.
+
+ If at any point :py:func:`pytest.exit` is called, the loop is
+ terminated immediately.
+
+ If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the
+ loop is terminated after the runtest protocol for the current item is finished.
+
+ :param session: The pytest session object.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> object | None:
+ """Perform the runtest protocol for a single test item.
+
+ The default runtest protocol is this (see individual hooks for full details):
+
+ - ``pytest_runtest_logstart(nodeid, location)``
+
+ - Setup phase:
+ - ``call = pytest_runtest_setup(item)`` (wrapped in ``CallInfo(when="setup")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - Call phase, if the setup passed and the ``setuponly`` pytest option is not set:
+ - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - Teardown phase:
+ - ``call = pytest_runtest_teardown(item, nextitem)`` (wrapped in ``CallInfo(when="teardown")``)
+ - ``report = pytest_runtest_makereport(item, call)``
+ - ``pytest_runtest_logreport(report)``
+ - ``pytest_exception_interact(call, report)`` if an interactive exception occurred
+
+ - ``pytest_runtest_logfinish(nodeid, location)``
+
+ :param item: Test item for which the runtest protocol is performed.
+ :param nextitem: The scheduled-to-be-next test item (or None if this is the end my friend).
+
+ Stops at first non-None result, see :ref:`firstresult`.
+ The return value is not used, but only stops further processing.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook.
+ """
+
+
+def pytest_runtest_logstart(nodeid: str, location: tuple[str, int | None, str]) -> None:
+ """Called at the start of running the runtest protocol for a single item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ :param nodeid: Full node ID of the item.
+ :param location: A tuple of ``(filename, lineno, testname)``
+ where ``filename`` is a file path relative to ``config.rootpath``
+ and ``lineno`` is 0-based.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+def pytest_runtest_logfinish(
+ nodeid: str, location: tuple[str, int | None, str]
+) -> None:
+ """Called at the end of running the runtest protocol for a single item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ :param nodeid: Full node ID of the item.
+ :param location: A tuple of ``(filename, lineno, testname)``
+ where ``filename`` is a file path relative to ``config.rootpath``
+ and ``lineno`` is 0-based.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+def pytest_runtest_setup(item: Item) -> None:
+ """Called to perform the setup phase for a test item.
+
+ The default implementation runs ``setup()`` on ``item`` and all of its
+ parents (which haven't been setup yet). This includes obtaining the
+ values of fixtures required by the item (which haven't been obtained
+ yet).
+
+ :param item:
+ The item.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+def pytest_runtest_call(item: Item) -> None:
+ """Called to run the test for test item (the call phase).
+
+ The default implementation calls ``item.runtest()``.
+
+ :param item:
+ The item.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
+ """Called to perform the teardown phase for a test item.
+
+ The default implementation runs the finalizers and calls ``teardown()``
+ on ``item`` and all of its parents (which need to be torn down). This
+ includes running the teardown phase of fixtures required by the item (if
+ they go out of scope).
+
+ :param item:
+ The item.
+ :param nextitem:
+ The scheduled-to-be-next test item (None if no further test item is
+ scheduled). This argument is used to perform exact teardowns, i.e.
+ calling just enough finalizers so that nextitem only needs to call
+ setup functions.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport | None:
+ """Called to create a :class:`~pytest.TestReport` for each of
+ the setup, call and teardown runtest phases of a test item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ :param item: The item.
+ :param call: The :class:`~pytest.CallInfo` for the phase.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+def pytest_runtest_logreport(report: TestReport) -> None:
+ """Process the :class:`~pytest.TestReport` produced for each
+ of the setup, call and teardown runtest phases of an item.
+
+ See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_to_serializable(
+ config: Config,
+ report: CollectReport | TestReport,
+) -> dict[str, Any] | None:
+ """Serialize the given report object into a data structure suitable for
+ sending over the wire, e.g. converted to JSON.
+
+ :param config: The pytest config object.
+ :param report: The report.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. The exact details may depend
+ on the plugin which calls the hook.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_from_serializable(
+ config: Config,
+ data: dict[str, Any],
+) -> CollectReport | TestReport | None:
+ """Restore a report object previously serialized with
+ :hook:`pytest_report_to_serializable`.
+
+ :param config: The pytest config object.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. The exact details may depend
+ on the plugin which calls the hook.
+ """
+
+
+# -------------------------------------------------------------------------
+# Fixture related hooks
+# -------------------------------------------------------------------------
+
+
+@hookspec(firstresult=True)
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[Any], request: SubRequest
+) -> object | None:
+ """Perform fixture setup execution.
+
+ :param fixturedef:
+ The fixture definition object.
+ :param request:
+ The fixture request object.
+ :returns:
+ The return value of the call to the fixture function.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ .. note::
+ If the fixture function returns None, other implementations of
+ this hook function will continue to be called, according to the
+ behavior of the :ref:`firstresult` option.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given fixture, only
+ conftest files in the fixture scope's directory and its parent directories
+ are consulted.
+ """
+
+
+def pytest_fixture_post_finalizer(
+ fixturedef: FixtureDef[Any], request: SubRequest
+) -> None:
+ """Called after fixture teardown, but before the cache is cleared, so
+ the fixture result ``fixturedef.cached_result`` is still available (not
+ ``None``).
+
+ :param fixturedef:
+ The fixture definition object.
+ :param request:
+ The fixture request object.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given fixture, only
+ conftest files in the fixture scope's directory and its parent directories
+ are consulted.
+ """
+
+
+# -------------------------------------------------------------------------
+# test session related hooks
+# -------------------------------------------------------------------------
+
+
+def pytest_sessionstart(session: Session) -> None:
+ """Called after the ``Session`` object has been created and before performing collection
+ and entering the run test loop.
+
+ :param session: The pytest session object.
+
+ Use in conftest plugins
+ =======================
+
+ This hook is only called for :ref:`initial conftests `.
+ """
+
+
+def pytest_sessionfinish(
+ session: Session,
+ exitstatus: int | ExitCode,
+) -> None:
+ """Called after whole test run finished, right before returning the exit status to the system.
+
+ :param session: The pytest session object.
+ :param exitstatus: The status which pytest will return to the system.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook.
+ """
+
+
+def pytest_unconfigure(config: Config) -> None:
+ """Called before test process is exited.
+
+ :param config: The pytest config object.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook.
+ """
+
+
+# -------------------------------------------------------------------------
+# hooks for customizing the assert methods
+# -------------------------------------------------------------------------
+
+
+def pytest_assertrepr_compare(
+ config: Config, op: str, left: object, right: object
+) -> list[str] | None:
+ """Return explanation for comparisons in failing assert expressions.
+
+ Return None for no custom explanation, otherwise return a list
+ of strings. The strings will be joined by newlines but any newlines
+ *in* a string will be escaped. Note that all but the first line will
+ be indented slightly, the intention is for the first line to be a summary.
+
+ :param config: The pytest config object.
+ :param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
+ :param left: The left operand.
+ :param right: The right operand.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None:
+ """Called whenever an assertion passes.
+
+ .. versionadded:: 5.0
+
+ Use this hook to do some processing after a passing assertion.
+ The original assertion information is available in the `orig` string
+ and the pytest introspected assertion information is available in the
+ `expl` string.
+
+ This hook must be explicitly enabled by the :confval:`enable_assertion_pass_hook`
+ configuration option:
+
+ .. tab:: toml
+
+ .. code-block:: toml
+
+ [pytest]
+ enable_assertion_pass_hook = true
+
+ .. tab:: ini
+
+ .. code-block:: ini
+
+ [pytest]
+ enable_assertion_pass_hook = true
+
+ You need to **clean the .pyc** files in your project directory and interpreter libraries
+ when enabling this option, as assertions will require to be re-written.
+
+ :param item: pytest item object of current test.
+ :param lineno: Line number of the assert statement.
+ :param orig: String with the original assertion.
+ :param expl: String with the assert explanation.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in the item's directory and its parent directories are consulted.
+ """
+
+
+# -------------------------------------------------------------------------
+# Hooks for influencing reporting (invoked from _pytest_terminal).
+# -------------------------------------------------------------------------
+
+
+def pytest_report_header(config: Config, start_path: Path) -> str | list[str]: # type: ignore[empty-body]
+ """Return a string or list of strings to be displayed as header info for terminal reporting.
+
+ :param config: The pytest config object.
+ :param start_path: The starting dir.
+ :type start_path: pathlib.Path
+
+ .. note::
+
+ Lines returned by a plugin are displayed before those of plugins which
+ ran before it.
+ If you want to have your line(s) displayed first, use
+ :ref:`trylast=True `.
+
+ .. versionchanged:: 7.0.0
+ The ``start_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``startdir`` parameter. The ``startdir`` parameter
+ has been deprecated and removed in pytest 9.0.0.
+
+ Use in conftest plugins
+ =======================
+
+ This hook is only called for :ref:`initial conftests `.
+ """
+
+
+def pytest_report_collectionfinish( # type: ignore[empty-body]
+ config: Config,
+ start_path: Path,
+ items: Sequence[Item],
+) -> str | list[str]:
+ """Return a string or list of strings to be displayed after collection
+ has finished successfully.
+
+ These strings will be displayed after the standard "collected X items" message.
+
+ .. versionadded:: 3.2
+
+ :param config: The pytest config object.
+ :param start_path: The starting dir.
+ :type start_path: pathlib.Path
+ :param items: List of pytest items that are going to be executed; this list should not be modified.
+
+ .. note::
+
+ Lines returned by a plugin are displayed before those of plugins which
+ ran before it.
+ If you want to have your line(s) displayed first, use
+ :ref:`trylast=True `.
+
+ .. versionchanged:: 7.0.0
+ The ``start_path`` parameter was added as a :class:`pathlib.Path`
+ equivalent of the ``startdir`` parameter. The ``startdir`` parameter
+ has been deprecated and removed in pytest 9.0.0.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+@hookspec(firstresult=True)
+def pytest_report_teststatus( # type:ignore[empty-body]
+ report: CollectReport | TestReport, config: Config
+) -> TestShortLogReport | tuple[str, str, str | tuple[str, Mapping[str, bool]]]:
+ """Return result-category, shortletter and verbose word for status
+ reporting.
+
+ The result-category is a category in which to count the result, for
+ example "passed", "skipped", "error" or the empty string.
+
+ The shortletter is shown as testing progresses, for example ".", "s",
+ "E" or the empty string.
+
+ The verbose word is shown as testing progresses in verbose mode, for
+ example "PASSED", "SKIPPED", "ERROR" or the empty string.
+
+ pytest may style these implicitly according to the report outcome.
+ To provide explicit styling, return a tuple for the verbose word,
+ for example ``"rerun", "R", ("RERUN", {"yellow": True})``.
+
+ :param report: The report object whose status is to be returned.
+ :param config: The pytest config object.
+ :returns: The test status.
+
+ Stops at first non-None result, see :ref:`firstresult`.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+def pytest_terminal_summary(
+ terminalreporter: TerminalReporter,
+ exitstatus: ExitCode,
+ config: Config,
+) -> None:
+ """Add a section to terminal summary reporting.
+
+ :param terminalreporter: The internal terminal reporter object.
+ :param exitstatus: The exit status that will be reported back to the OS.
+ :param config: The pytest config object.
+
+ .. versionadded:: 4.2
+ The ``config`` parameter.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+@hookspec(historic=True)
+def pytest_warning_recorded(
+ warning_message: warnings.WarningMessage,
+ when: Literal["config", "collect", "runtest"],
+ nodeid: str,
+ location: tuple[str, int, str] | None,
+) -> None:
+ """Process a warning captured by the internal pytest warnings plugin.
+
+ :param warning_message:
+ The captured warning. This is the same object produced by :class:`warnings.catch_warnings`,
+ and contains the same attributes as the parameters of :py:func:`warnings.showwarning`.
+
+ :param when:
+ Indicates when the warning was captured. Possible values:
+
+ * ``"config"``: during pytest configuration/initialization stage.
+ * ``"collect"``: during test collection.
+ * ``"runtest"``: during test execution.
+
+ :param nodeid:
+ Full id of the item. Empty string for warnings that are not specific to
+ a particular node.
+
+ :param location:
+ When available, holds information about the execution context of the captured
+ warning (filename, linenumber, function). ``function`` evaluates to
+ when the execution context is at the module level.
+
+ .. versionadded:: 6.0
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. If the warning is specific to a
+ particular node, only conftest files in parent directories of the node are
+ consulted.
+ """
+
+
+# -------------------------------------------------------------------------
+# Hooks for influencing skipping
+# -------------------------------------------------------------------------
+
+
+def pytest_markeval_namespace( # type:ignore[empty-body]
+ config: Config,
+) -> dict[str, Any]:
+ """Called when constructing the globals dictionary used for
+ evaluating string conditions in xfail/skipif markers.
+
+ This is useful when the condition for a marker requires
+ objects that are expensive or impossible to obtain during
+ collection time, which is required by normal boolean
+ conditions.
+
+ .. versionadded:: 6.2
+
+ :param config: The pytest config object.
+ :returns: A dictionary of additional globals to add.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given item, only conftest
+ files in parent directories of the item are consulted.
+ """
+
+
+# -------------------------------------------------------------------------
+# error handling and internal debugging hooks
+# -------------------------------------------------------------------------
+
+
+def pytest_internalerror(
+ excrepr: ExceptionRepr,
+ excinfo: ExceptionInfo[BaseException],
+) -> bool | None:
+ """Called for internal errors.
+
+ Return True to suppress the fallback handling of printing an
+ INTERNALERROR message directly to sys.stderr.
+
+ :param excrepr: The exception repr object.
+ :param excinfo: The exception info.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+def pytest_keyboard_interrupt(
+ excinfo: ExceptionInfo[KeyboardInterrupt | Exit],
+) -> None:
+ """Called for keyboard interrupt.
+
+ :param excinfo: The exception info.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+def pytest_exception_interact(
+ node: Item | Collector,
+ call: CallInfo[Any],
+ report: CollectReport | TestReport,
+) -> None:
+ """Called when an exception was raised which can potentially be
+ interactively handled.
+
+ May be called during collection (see :hook:`pytest_make_collect_report`),
+ in which case ``report`` is a :class:`~pytest.CollectReport`.
+
+ May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
+ in which case ``report`` is a :class:`~pytest.TestReport`.
+
+ This hook is not called if the exception that was raised is an internal
+ exception like ``skip.Exception``.
+
+ :param node:
+ The item or collector.
+ :param call:
+ The call information. Contains the exception.
+ :param report:
+ The collection or test report.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest file can implement this hook. For a given node, only conftest
+ files in parent directories of the node are consulted.
+ """
+
+
+def pytest_enter_pdb(config: Config, pdb: pdb.Pdb) -> None:
+ """Called upon pdb.set_trace().
+
+ Can be used by plugins to take special action just before the python
+ debugger enters interactive mode.
+
+ :param config: The pytest config object.
+ :param pdb: The Pdb instance.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
+
+
+def pytest_leave_pdb(config: Config, pdb: pdb.Pdb) -> None:
+ """Called when leaving pdb (e.g. with continue after pdb.set_trace()).
+
+ Can be used by plugins to take special action just after the python
+ debugger leaves interactive mode.
+
+ :param config: The pytest config object.
+ :param pdb: The Pdb instance.
+
+ Use in conftest plugins
+ =======================
+
+ Any conftest plugin can implement this hook.
+ """
diff --git a/venv/lib/python3.11/site-packages/_pytest/junitxml.py b/venv/lib/python3.11/site-packages/_pytest/junitxml.py
new file mode 100644
index 000000000..ac78f50e6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/junitxml.py
@@ -0,0 +1,703 @@
+# mypy: allow-untyped-defs
+"""Report test results in JUnit-XML format, for use with Jenkins and build
+integration servers.
+
+Based on initial code from Ross Lawley.
+
+Output conforms to
+https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+import functools
+import os
+import platform
+import re
+import xml.etree.ElementTree as ET
+
+from _pytest import nodes
+from _pytest import timing
+from _pytest._code.code import ExceptionRepr
+from _pytest._code.code import ReprFileLocation
+from _pytest.config import Config
+from _pytest.config import filename_arg
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureRequest
+from _pytest.reports import TestReport
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+import pytest
+
+
+xml_key = StashKey["LogXML"]()
+
+
+def bin_xml_escape(arg: object) -> str:
+ r"""Visually escape invalid XML characters.
+
+ For example, transforms
+ 'hello\aworld\b'
+ into
+ 'hello#x07world#x08'
+ Note that the #xABs are *not* XML escapes - missing the ampersand «.
+ The idea is to escape visually for the user rather than for XML itself.
+ """
+
+ def repl(matchobj: re.Match[str]) -> str:
+ i = ord(matchobj.group())
+ if i <= 0xFF:
+ return f"#x{i:02X}"
+ else:
+ return f"#x{i:04X}"
+
+ # The spec range of valid chars is:
+ # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+ # For an unknown(?) reason, we disallow #x7F (DEL) as well.
+ illegal_xml_re = "[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\U00010000-\U0010ffff]"
+ return re.sub(illegal_xml_re, repl, str(arg))
+
+
+def merge_family(left, right) -> None:
+ result = {}
+ for kl, vl in left.items():
+ for kr, vr in right.items():
+ if not isinstance(vl, list):
+ raise TypeError(type(vl))
+ result[kl] = vl + vr
+ left.update(result)
+
+
+families = { # pylint: disable=dict-init-mutate
+ "_base": {"testcase": ["classname", "name"]},
+ "_base_legacy": {"testcase": ["file", "line", "url"]},
+}
+# xUnit 1.x inherits legacy attributes.
+families["xunit1"] = families["_base"].copy()
+merge_family(families["xunit1"], families["_base_legacy"])
+
+# xUnit 2.x uses strict base attributes.
+families["xunit2"] = families["_base"]
+
+
+class _NodeReporter:
+ def __init__(self, nodeid: str | TestReport, xml: LogXML) -> None:
+ self.id = nodeid
+ self.xml = xml
+ self.add_stats = self.xml.add_stats
+ self.family = self.xml.family
+ self.duration = 0.0
+ self.properties: list[tuple[str, str]] = []
+ self.nodes: list[ET.Element] = []
+ self.attrs: dict[str, str] = {}
+
+ def append(self, node: ET.Element) -> None:
+ self.xml.add_stats(node.tag)
+ self.nodes.append(node)
+
+ def add_property(self, name: str, value: object) -> None:
+ self.properties.append((str(name), bin_xml_escape(value)))
+
+ def add_attribute(self, name: str, value: object) -> None:
+ self.attrs[str(name)] = bin_xml_escape(value)
+
+ def make_properties_node(self) -> ET.Element | None:
+ """Return a Junit node containing custom properties, if any."""
+ if self.properties:
+ properties = ET.Element("properties")
+ for name, value in self.properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
+
+ def record_testreport(self, testreport: TestReport) -> None:
+ names = mangle_test_address(testreport.nodeid)
+ existing_attrs = self.attrs
+ classnames = names[:-1]
+ if self.xml.prefix:
+ classnames.insert(0, self.xml.prefix)
+ attrs: dict[str, str] = {
+ "classname": ".".join(classnames),
+ "name": bin_xml_escape(names[-1]),
+ "file": testreport.location[0],
+ }
+ if testreport.location[1] is not None:
+ attrs["line"] = str(testreport.location[1])
+ if hasattr(testreport, "url"):
+ attrs["url"] = testreport.url
+ self.attrs = attrs
+ self.attrs.update(existing_attrs) # Restore any user-defined attributes.
+
+ # Preserve legacy testcase behavior.
+ if self.family == "xunit1":
+ return
+
+ # Filter out attributes not permitted by this test family.
+ # Including custom attributes because they are not valid here.
+ temp_attrs = {}
+ for key in self.attrs:
+ if key in families[self.family]["testcase"]:
+ temp_attrs[key] = self.attrs[key]
+ self.attrs = temp_attrs
+
+ def to_xml(self) -> ET.Element:
+ testcase = ET.Element("testcase", self.attrs, time=f"{self.duration:.3f}")
+ properties = self.make_properties_node()
+ if properties is not None:
+ testcase.append(properties)
+ testcase.extend(self.nodes)
+ return testcase
+
+ def _add_simple(self, tag: str, message: str, data: str | None = None) -> None:
+ node = ET.Element(tag, message=message)
+ node.text = bin_xml_escape(data)
+ self.append(node)
+
+ def write_captured_output(self, report: TestReport) -> None:
+ if not self.xml.log_passing_tests and report.passed:
+ return
+
+ content_out = report.capstdout
+ content_log = report.caplog
+ content_err = report.capstderr
+ if self.xml.logging == "no":
+ return
+ content_all = ""
+ if self.xml.logging in ["log", "all"]:
+ content_all = self._prepare_content(content_log, " Captured Log ")
+ if self.xml.logging in ["system-out", "out-err", "all"]:
+ content_all += self._prepare_content(content_out, " Captured Out ")
+ self._write_content(report, content_all, "system-out")
+ content_all = ""
+ if self.xml.logging in ["system-err", "out-err", "all"]:
+ content_all += self._prepare_content(content_err, " Captured Err ")
+ self._write_content(report, content_all, "system-err")
+ content_all = ""
+ if content_all:
+ self._write_content(report, content_all, "system-out")
+
+ def _prepare_content(self, content: str, header: str) -> str:
+ return "\n".join([header.center(80, "-"), content, ""])
+
+ def _write_content(self, report: TestReport, content: str, jheader: str) -> None:
+ tag = ET.Element(jheader)
+ tag.text = bin_xml_escape(content)
+ self.append(tag)
+
+ def append_pass(self, report: TestReport) -> None:
+ self.add_stats("passed")
+
+ def append_failure(self, report: TestReport) -> None:
+ # msg = str(report.longrepr.reprtraceback.extraline)
+ if hasattr(report, "wasxfail"):
+ self._add_simple("skipped", "xfail-marked test passes unexpectedly")
+ else:
+ assert report.longrepr is not None
+ reprcrash: ReprFileLocation | None = getattr(
+ report.longrepr, "reprcrash", None
+ )
+ if reprcrash is not None:
+ message = reprcrash.message
+ else:
+ message = str(report.longrepr)
+ message = bin_xml_escape(message)
+ self._add_simple("failure", message, str(report.longrepr))
+
+ def append_collect_error(self, report: TestReport) -> None:
+ # msg = str(report.longrepr.reprtraceback.extraline)
+ assert report.longrepr is not None
+ self._add_simple("error", "collection failure", str(report.longrepr))
+
+ def append_collect_skipped(self, report: TestReport) -> None:
+ self._add_simple("skipped", "collection skipped", str(report.longrepr))
+
+ def append_error(self, report: TestReport) -> None:
+ assert report.longrepr is not None
+ reprcrash: ReprFileLocation | None = getattr(report.longrepr, "reprcrash", None)
+ if reprcrash is not None:
+ reason = reprcrash.message
+ else:
+ reason = str(report.longrepr)
+
+ if report.when == "teardown":
+ msg = f'failed on teardown with "{reason}"'
+ else:
+ msg = f'failed on setup with "{reason}"'
+ self._add_simple("error", bin_xml_escape(msg), str(report.longrepr))
+
+ def append_skipped(self, report: TestReport) -> None:
+ if hasattr(report, "wasxfail"):
+ xfailreason = report.wasxfail
+ if xfailreason.startswith("reason: "):
+ xfailreason = xfailreason[8:]
+ xfailreason = bin_xml_escape(xfailreason)
+ skipped = ET.Element("skipped", type="pytest.xfail", message=xfailreason)
+ self.append(skipped)
+ else:
+ assert isinstance(report.longrepr, tuple)
+ filename, lineno, skipreason = report.longrepr
+ if skipreason.startswith("Skipped: "):
+ skipreason = skipreason[9:]
+ details = f"{filename}:{lineno}: {skipreason}"
+
+ skipped = ET.Element(
+ "skipped", type="pytest.skip", message=bin_xml_escape(skipreason)
+ )
+ skipped.text = bin_xml_escape(details)
+ self.append(skipped)
+ self.write_captured_output(report)
+
+ def finalize(self) -> None:
+ data = self.to_xml()
+ self.__dict__.clear()
+ # Type ignored because mypy doesn't like overriding a method.
+ # Also the return value doesn't match...
+ self.to_xml = lambda: data # type: ignore[method-assign]
+
+
+def _warn_incompatibility_with_xunit2(
+ request: FixtureRequest, fixture_name: str
+) -> None:
+ """Emit a PytestWarning about the given fixture being incompatible with newer xunit revisions."""
+ from _pytest.warning_types import PytestWarning
+
+ xml = request.config.stash.get(xml_key, None)
+ if xml is not None and xml.family not in ("xunit1", "legacy"):
+ request.node.warn(
+ PytestWarning(
+ f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')"
+ )
+ )
+
+
+@pytest.fixture
+def record_property(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Add extra properties to the calling test.
+
+ User properties become part of the test report and are available to the
+ configured reporters, like JUnit XML.
+
+ The fixture is callable with ``name, value``. The value is automatically
+ XML-encoded.
+
+ Example::
+
+ def test_function(record_property):
+ record_property("example_key", 1)
+ """
+ _warn_incompatibility_with_xunit2(request, "record_property")
+
+ def append_property(name: str, value: object) -> None:
+ request.node.user_properties.append((name, value))
+
+ return append_property
+
+
+@pytest.fixture
+def record_xml_attribute(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Add extra xml attributes to the tag for the calling test.
+
+ The fixture is callable with ``name, value``. The value is
+ automatically XML-encoded.
+ """
+ from _pytest.warning_types import PytestExperimentalApiWarning
+
+ request.node.warn(
+ PytestExperimentalApiWarning("record_xml_attribute is an experimental feature")
+ )
+
+ _warn_incompatibility_with_xunit2(request, "record_xml_attribute")
+
+ # Declare noop
+ def add_attr_noop(name: str, value: object) -> None:
+ pass
+
+ attr_func = add_attr_noop
+
+ xml = request.config.stash.get(xml_key, None)
+ if xml is not None:
+ node_reporter = xml.node_reporter(request.node.nodeid)
+ attr_func = node_reporter.add_attribute
+
+ return attr_func
+
+
+def _check_record_param_type(param: str, v: str) -> None:
+ """Used by record_testsuite_property to check that the given parameter name is of the proper
+ type."""
+ __tracebackhide__ = True
+ if not isinstance(v, str):
+ msg = "{param} parameter needs to be a string, but {g} given" # type: ignore[unreachable]
+ raise TypeError(msg.format(param=param, g=type(v).__name__))
+
+
+@pytest.fixture(scope="session")
+def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object], None]:
+ """Record a new ```` tag as child of the root ````.
+
+ This is suitable to writing global information regarding the entire test
+ suite, and is compatible with ``xunit2`` JUnit family.
+
+ This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
+
+ .. code-block:: python
+
+ def test_foo(record_testsuite_property):
+ record_testsuite_property("ARCH", "PPC")
+ record_testsuite_property("STORAGE_TYPE", "CEPH")
+
+ :param name:
+ The property name.
+ :param value:
+ The property value. Will be converted to a string.
+
+ .. warning::
+
+ Currently this fixture **does not work** with the
+ `pytest-xdist `__ plugin. See
+ :issue:`7767` for details.
+ """
+ __tracebackhide__ = True
+
+ def record_func(name: str, value: object) -> None:
+ """No-op function in case --junit-xml was not passed in the command-line."""
+ __tracebackhide__ = True
+ _check_record_param_type("name", name)
+
+ xml = request.config.stash.get(xml_key, None)
+ if xml is not None:
+ record_func = xml.add_global_property
+ return record_func
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting")
+ group.addoption(
+ "--junitxml",
+ "--junit-xml",
+ action="store",
+ dest="xmlpath",
+ metavar="path",
+ type=functools.partial(filename_arg, optname="--junitxml"),
+ default=None,
+ help="Create junit-xml style report file at given path",
+ )
+ group.addoption(
+ "--junitprefix",
+ "--junit-prefix",
+ action="store",
+ metavar="str",
+ default=None,
+ help="Prepend prefix to classnames in junit-xml output",
+ )
+ parser.addini(
+ "junit_suite_name", "Test suite name for JUnit report", default="pytest"
+ )
+ parser.addini(
+ "junit_logging",
+ "Write captured log messages to JUnit report: "
+ "one of no|log|system-out|system-err|out-err|all",
+ default="no",
+ )
+ parser.addini(
+ "junit_log_passing_tests",
+ "Capture log information for passing tests to JUnit report: ",
+ type="bool",
+ default=True,
+ )
+ parser.addini(
+ "junit_duration_report",
+ "Duration time to report: one of total|call",
+ default="total",
+ ) # choices=['total', 'call'])
+ parser.addini(
+ "junit_family",
+ "Emit XML for schema: one of legacy|xunit1|xunit2",
+ default="xunit2",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ xmlpath = config.option.xmlpath
+ # Prevent opening xmllog on worker nodes (xdist).
+ if xmlpath and not hasattr(config, "workerinput"):
+ junit_family = config.getini("junit_family")
+ config.stash[xml_key] = LogXML(
+ xmlpath,
+ config.option.junitprefix,
+ config.getini("junit_suite_name"),
+ config.getini("junit_logging"),
+ config.getini("junit_duration_report"),
+ junit_family,
+ config.getini("junit_log_passing_tests"),
+ )
+ config.pluginmanager.register(config.stash[xml_key])
+
+
+def pytest_unconfigure(config: Config) -> None:
+ xml = config.stash.get(xml_key, None)
+ if xml:
+ del config.stash[xml_key]
+ config.pluginmanager.unregister(xml)
+
+
+def mangle_test_address(address: str) -> list[str]:
+ path, possible_open_bracket, params = address.partition("[")
+ names = path.split("::")
+ # Convert file path to dotted path.
+ names[0] = names[0].replace(nodes.SEP, ".")
+ names[0] = re.sub(r"\.py$", "", names[0])
+ # Put any params back.
+ names[-1] += possible_open_bracket + params
+ return names
+
+
+class LogXML:
+ def __init__(
+ self,
+ logfile,
+ prefix: str | None,
+ suite_name: str = "pytest",
+ logging: str = "no",
+ report_duration: str = "total",
+ family="xunit1",
+ log_passing_tests: bool = True,
+ ) -> None:
+ logfile = os.path.expanduser(os.path.expandvars(logfile))
+ self.logfile = os.path.normpath(os.path.abspath(logfile))
+ self.prefix = prefix
+ self.suite_name = suite_name
+ self.logging = logging
+ self.log_passing_tests = log_passing_tests
+ self.report_duration = report_duration
+ self.family = family
+ self.stats: dict[str, int] = dict.fromkeys(
+ ["error", "passed", "failure", "skipped"], 0
+ )
+ self.node_reporters: dict[tuple[str | TestReport, object], _NodeReporter] = {}
+ self.node_reporters_ordered: list[_NodeReporter] = []
+ self.global_properties: list[tuple[str, str]] = []
+
+ # List of reports that failed on call but teardown is pending.
+ self.open_reports: list[TestReport] = []
+ self.cnt_double_fail_tests = 0
+
+ # Replaces convenience family with real family.
+ if self.family == "legacy":
+ self.family = "xunit1"
+
+ def finalize(self, report: TestReport) -> None:
+ nodeid = getattr(report, "nodeid", report)
+ # Local hack to handle xdist report order.
+ workernode = getattr(report, "node", None)
+ reporter = self.node_reporters.pop((nodeid, workernode))
+
+ for propname, propvalue in report.user_properties:
+ reporter.add_property(propname, str(propvalue))
+
+ if reporter is not None:
+ reporter.finalize()
+
+ def node_reporter(self, report: TestReport | str) -> _NodeReporter:
+ nodeid: str | TestReport = getattr(report, "nodeid", report)
+ # Local hack to handle xdist report order.
+ workernode = getattr(report, "node", None)
+
+ key = nodeid, workernode
+
+ if key in self.node_reporters:
+ # TODO: breaks for --dist=each
+ return self.node_reporters[key]
+
+ reporter = _NodeReporter(nodeid, self)
+
+ self.node_reporters[key] = reporter
+ self.node_reporters_ordered.append(reporter)
+
+ return reporter
+
+ def add_stats(self, key: str) -> None:
+ if key in self.stats:
+ self.stats[key] += 1
+
+ def _opentestcase(self, report: TestReport) -> _NodeReporter:
+ reporter = self.node_reporter(report)
+ reporter.record_testreport(report)
+ return reporter
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ """Handle a setup/call/teardown report, generating the appropriate
+ XML tags as necessary.
+
+ Note: due to plugins like xdist, this hook may be called in interlaced
+ order with reports from other nodes. For example:
+
+ Usual call order:
+ -> setup node1
+ -> call node1
+ -> teardown node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+
+ Possible call order in xdist:
+ -> setup node1
+ -> call node1
+ -> setup node2
+ -> call node2
+ -> teardown node2
+ -> teardown node1
+ """
+ close_report = None
+ if report.passed:
+ if report.when == "call": # ignore setup/teardown
+ reporter = self._opentestcase(report)
+ reporter.append_pass(report)
+ elif report.failed:
+ if report.when == "teardown":
+ # The following vars are needed when xdist plugin is used.
+ report_wid = getattr(report, "worker_id", None)
+ report_ii = getattr(report, "item_index", None)
+ close_report = next(
+ (
+ rep
+ for rep in self.open_reports
+ if (
+ rep.nodeid == report.nodeid
+ and getattr(rep, "item_index", None) == report_ii
+ and getattr(rep, "worker_id", None) == report_wid
+ )
+ ),
+ None,
+ )
+ if close_report:
+ # We need to open new testcase in case we have failure in
+ # call and error in teardown in order to follow junit
+ # schema.
+ self.finalize(close_report)
+ else:
+ # A passing call with a teardown error creates separate
+ # terminal reports, but JUnit XML keeps one testcase
+ # element for that item (#3850).
+ self.cnt_double_fail_tests += int(
+ (
+ report.nodeid,
+ getattr(report, "node", None),
+ )
+ in self.node_reporters
+ )
+ reporter = self._opentestcase(report)
+ if report.when == "call":
+ reporter.append_failure(report)
+ self.open_reports.append(report)
+ if not self.log_passing_tests:
+ reporter.write_captured_output(report)
+ else:
+ reporter.append_error(report)
+ elif report.skipped:
+ reporter = self._opentestcase(report)
+ reporter.append_skipped(report)
+ self.update_testcase_duration(report)
+ if report.when == "teardown":
+ reporter = self._opentestcase(report)
+ reporter.write_captured_output(report)
+
+ self.finalize(report)
+ report_wid = getattr(report, "worker_id", None)
+ report_ii = getattr(report, "item_index", None)
+ close_report = next(
+ (
+ rep
+ for rep in self.open_reports
+ if (
+ rep.nodeid == report.nodeid
+ and getattr(rep, "item_index", None) == report_ii
+ and getattr(rep, "worker_id", None) == report_wid
+ )
+ ),
+ None,
+ )
+ if close_report:
+ self.open_reports.remove(close_report)
+
+ def update_testcase_duration(self, report: TestReport) -> None:
+ """Accumulate total duration for nodeid from given report and update
+ the Junit.testcase with the new total if already created."""
+ if self.report_duration in {"total", report.when}:
+ reporter = self.node_reporter(report)
+ reporter.duration += getattr(report, "duration", 0.0)
+
+ def pytest_collectreport(self, report: TestReport) -> None:
+ if not report.passed:
+ reporter = self._opentestcase(report)
+ if report.failed:
+ reporter.append_collect_error(report)
+ else:
+ reporter.append_collect_skipped(report)
+
+ def pytest_internalerror(self, excrepr: ExceptionRepr) -> None:
+ reporter = self.node_reporter("internal")
+ reporter.attrs.update(classname="pytest", name="internal")
+ reporter._add_simple("error", "internal error", str(excrepr))
+
+ def pytest_sessionstart(self) -> None:
+ self.suite_start = timing.Instant()
+
+ def pytest_sessionfinish(self) -> None:
+ dirname = os.path.dirname(os.path.abspath(self.logfile))
+ # exist_ok avoids filesystem race conditions between checking path existence and requesting creation
+ os.makedirs(dirname, exist_ok=True)
+
+ with open(self.logfile, "w", encoding="utf-8") as logfile:
+ duration = self.suite_start.elapsed()
+
+ numtests = (
+ self.stats["passed"]
+ + self.stats["failure"]
+ + self.stats["skipped"]
+ + self.stats["error"]
+ - self.cnt_double_fail_tests
+ )
+ logfile.write('')
+
+ suite_node = ET.Element(
+ "testsuite",
+ name=self.suite_name,
+ errors=str(self.stats["error"]),
+ failures=str(self.stats["failure"]),
+ skipped=str(self.stats["skipped"]),
+ tests=str(numtests),
+ time=f"{duration.seconds:.3f}",
+ timestamp=self.suite_start.as_utc().astimezone().isoformat(),
+ hostname=platform.node(),
+ )
+ global_properties = self._get_global_properties_node()
+ if global_properties is not None:
+ suite_node.append(global_properties)
+ for node_reporter in self.node_reporters_ordered:
+ suite_node.append(node_reporter.to_xml())
+ testsuites = ET.Element("testsuites")
+ testsuites.set("name", "pytest tests")
+ testsuites.append(suite_node)
+ logfile.write(ET.tostring(testsuites, encoding="unicode"))
+
+ def pytest_terminal_summary(
+ self, terminalreporter: TerminalReporter, config: pytest.Config
+ ) -> None:
+ if config.get_verbosity() >= 0:
+ terminalreporter.write_sep("-", f"generated xml file: {self.logfile}")
+
+ def add_global_property(self, name: str, value: object) -> None:
+ __tracebackhide__ = True
+ _check_record_param_type("name", name)
+ self.global_properties.append((name, bin_xml_escape(value)))
+
+ def _get_global_properties_node(self) -> ET.Element | None:
+ """Return a Junit node containing custom properties, if any."""
+ if self.global_properties:
+ properties = ET.Element("properties")
+ for name, value in self.global_properties:
+ properties.append(ET.Element("property", name=name, value=value))
+ return properties
+ return None
diff --git a/venv/lib/python3.11/site-packages/_pytest/legacypath.py b/venv/lib/python3.11/site-packages/_pytest/legacypath.py
new file mode 100644
index 000000000..59e8ef6e7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/legacypath.py
@@ -0,0 +1,468 @@
+# mypy: allow-untyped-defs
+"""Add backward compatibility support for the legacy py path type."""
+
+from __future__ import annotations
+
+import dataclasses
+from pathlib import Path
+import shlex
+import subprocess
+from typing import Final
+from typing import final
+from typing import TYPE_CHECKING
+
+from iniconfig import SectionWrapper
+
+from _pytest.cacheprovider import Cache
+from _pytest.compat import LEGACY_PATH
+from _pytest.compat import legacy_path
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.pytester import HookRecorder
+from _pytest.pytester import Pytester
+from _pytest.pytester import RunResult
+from _pytest.terminal import TerminalReporter
+from _pytest.tmpdir import TempPathFactory
+
+
+if TYPE_CHECKING:
+ import pexpect
+
+
+@final
+class Testdir:
+ """
+ Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead.
+
+ All methods just forward to an internal :class:`Pytester` instance, converting results
+ to `legacy_path` objects as necessary.
+ """
+
+ __test__ = False
+
+ CLOSE_STDIN: Final = Pytester.CLOSE_STDIN
+ TimeoutExpired: Final = Pytester.TimeoutExpired
+
+ def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._pytester = pytester
+
+ @property
+ def tmpdir(self) -> LEGACY_PATH:
+ """Temporary directory where tests are executed."""
+ return legacy_path(self._pytester.path)
+
+ @property
+ def test_tmproot(self) -> LEGACY_PATH:
+ return legacy_path(self._pytester._test_tmproot)
+
+ @property
+ def request(self):
+ return self._pytester._request
+
+ @property
+ def plugins(self):
+ return self._pytester.plugins
+
+ @plugins.setter
+ def plugins(self, plugins):
+ self._pytester.plugins = plugins
+
+ @property
+ def monkeypatch(self) -> MonkeyPatch:
+ return self._pytester._monkeypatch
+
+ def make_hook_recorder(self, pluginmanager) -> HookRecorder:
+ """See :meth:`Pytester.make_hook_recorder`."""
+ return self._pytester.make_hook_recorder(pluginmanager)
+
+ def chdir(self) -> None:
+ """See :meth:`Pytester.chdir`."""
+ return self._pytester.chdir()
+
+ def finalize(self) -> None:
+ return self._pytester._finalize()
+
+ def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.makefile`."""
+ if ext and not ext.startswith("."):
+ # pytester.makefile is going to throw a ValueError in a way that
+ # testdir.makefile did not, because
+ # pathlib.Path is stricter suffixes than py.path
+ # This ext arguments is likely user error, but since testdir has
+ # allowed this, we will prepend "." as a workaround to avoid breaking
+ # testdir usage that worked before
+ ext = "." + ext
+ return legacy_path(self._pytester.makefile(ext, *args, **kwargs))
+
+ def makeconftest(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makeconftest`."""
+ return legacy_path(self._pytester.makeconftest(source))
+
+ def makeini(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makeini`."""
+ return legacy_path(self._pytester.makeini(source))
+
+ def getinicfg(self, source: str) -> SectionWrapper:
+ """See :meth:`Pytester.getinicfg`."""
+ return self._pytester.getinicfg(source)
+
+ def makepyprojecttoml(self, source) -> LEGACY_PATH:
+ """See :meth:`Pytester.makepyprojecttoml`."""
+ return legacy_path(self._pytester.makepyprojecttoml(source))
+
+ def makepyfile(self, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.makepyfile`."""
+ return legacy_path(self._pytester.makepyfile(*args, **kwargs))
+
+ def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH:
+ """See :meth:`Pytester.maketxtfile`."""
+ return legacy_path(self._pytester.maketxtfile(*args, **kwargs))
+
+ def syspathinsert(self, path=None) -> None:
+ """See :meth:`Pytester.syspathinsert`."""
+ return self._pytester.syspathinsert(path)
+
+ def mkdir(self, name) -> LEGACY_PATH:
+ """See :meth:`Pytester.mkdir`."""
+ return legacy_path(self._pytester.mkdir(name))
+
+ def mkpydir(self, name) -> LEGACY_PATH:
+ """See :meth:`Pytester.mkpydir`."""
+ return legacy_path(self._pytester.mkpydir(name))
+
+ def copy_example(self, name=None) -> LEGACY_PATH:
+ """See :meth:`Pytester.copy_example`."""
+ return legacy_path(self._pytester.copy_example(name))
+
+ def getnode(self, config: Config, arg) -> Item | Collector | None:
+ """See :meth:`Pytester.getnode`."""
+ return self._pytester.getnode(config, arg)
+
+ def getpathnode(self, path):
+ """See :meth:`Pytester.getpathnode`."""
+ return self._pytester.getpathnode(path)
+
+ def genitems(self, colitems: list[Item | Collector]) -> list[Item]:
+ """See :meth:`Pytester.genitems`."""
+ return self._pytester.genitems(colitems)
+
+ def runitem(self, source):
+ """See :meth:`Pytester.runitem`."""
+ return self._pytester.runitem(source)
+
+ def inline_runsource(self, source, *cmdlineargs):
+ """See :meth:`Pytester.inline_runsource`."""
+ return self._pytester.inline_runsource(source, *cmdlineargs)
+
+ def inline_genitems(self, *args):
+ """See :meth:`Pytester.inline_genitems`."""
+ return self._pytester.inline_genitems(*args)
+
+ def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
+ """See :meth:`Pytester.inline_run`."""
+ return self._pytester.inline_run(
+ *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
+ )
+
+ def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
+ """See :meth:`Pytester.runpytest_inprocess`."""
+ return self._pytester.runpytest_inprocess(*args, **kwargs)
+
+ def runpytest(self, *args, **kwargs) -> RunResult:
+ """See :meth:`Pytester.runpytest`."""
+ return self._pytester.runpytest(*args, **kwargs)
+
+ def parseconfig(self, *args) -> Config:
+ """See :meth:`Pytester.parseconfig`."""
+ return self._pytester.parseconfig(*args)
+
+ def parseconfigure(self, *args) -> Config:
+ """See :meth:`Pytester.parseconfigure`."""
+ return self._pytester.parseconfigure(*args)
+
+ def getitem(self, source, funcname="test_func"):
+ """See :meth:`Pytester.getitem`."""
+ return self._pytester.getitem(source, funcname)
+
+ def getitems(self, source):
+ """See :meth:`Pytester.getitems`."""
+ return self._pytester.getitems(source)
+
+ def getmodulecol(self, source, configargs=(), withinit=False):
+ """See :meth:`Pytester.getmodulecol`."""
+ return self._pytester.getmodulecol(
+ source, configargs=configargs, withinit=withinit
+ )
+
+ def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None:
+ """See :meth:`Pytester.collect_by_name`."""
+ return self._pytester.collect_by_name(modcol, name)
+
+ def popen(
+ self,
+ cmdargs,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ stdin=CLOSE_STDIN,
+ **kw,
+ ):
+ """See :meth:`Pytester.popen`."""
+ return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
+
+ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
+ """See :meth:`Pytester.run`."""
+ return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
+
+ def runpython(self, script) -> RunResult:
+ """See :meth:`Pytester.runpython`."""
+ return self._pytester.runpython(script)
+
+ def runpython_c(self, command):
+ """See :meth:`Pytester.runpython_c`."""
+ return self._pytester.runpython_c(command)
+
+ def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
+ """See :meth:`Pytester.runpytest_subprocess`."""
+ return self._pytester.runpytest_subprocess(*args, timeout=timeout)
+
+ def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn:
+ """See :meth:`Pytester.spawn_pytest`."""
+ return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
+
+ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn:
+ """See :meth:`Pytester.spawn`."""
+ return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
+
+ def __repr__(self) -> str:
+ return f""
+
+ def __str__(self) -> str:
+ return str(self.tmpdir)
+
+
+class LegacyTestdirPlugin:
+ @staticmethod
+ @fixture
+ def testdir(pytester: Pytester) -> Testdir:
+ """
+ Identical to :fixture:`pytester`, and provides an instance whose methods return
+ legacy ``LEGACY_PATH`` objects instead when applicable.
+
+ New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
+ """
+ return Testdir(pytester, _ispytest=True)
+
+
+@final
+@dataclasses.dataclass
+class TempdirFactory:
+ """Backward compatibility wrapper that implements ``py.path.local``
+ for :class:`TempPathFactory`.
+
+ .. note::
+ These days, it is preferred to use ``tmp_path_factory``.
+
+ :ref:`About the tmpdir and tmpdir_factory fixtures`.
+
+ """
+
+ _tmppath_factory: TempPathFactory
+
+ def __init__(
+ self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._tmppath_factory = tmppath_factory
+
+ def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
+ """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object."""
+ return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
+
+ def getbasetemp(self) -> LEGACY_PATH:
+ """Same as :meth:`TempPathFactory.getbasetemp`, but returns a ``py.path.local`` object."""
+ return legacy_path(self._tmppath_factory.getbasetemp().resolve())
+
+
+class LegacyTmpdirPlugin:
+ @staticmethod
+ @fixture(scope="session")
+ def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
+ """Return a :class:`pytest.TempdirFactory` instance for the test session."""
+ # Set dynamically by pytest_configure().
+ return request.config._tmpdirhandler # type: ignore
+
+ @staticmethod
+ @fixture
+ def tmpdir(tmp_path: Path) -> LEGACY_PATH:
+ """Return a temporary directory (as `legacy_path`_ object)
+ which is unique to each test function invocation.
+ The temporary directory is created as a subdirectory
+ of the base temporary directory, with configurable retention,
+ as discussed in :ref:`temporary directory location and retention`.
+
+ .. note::
+ These days, it is preferred to use ``tmp_path``.
+
+ :ref:`About the tmpdir and tmpdir_factory fixtures`.
+
+ .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
+ """
+ return legacy_path(tmp_path)
+
+
+def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH:
+ """Return a directory path object with the given name.
+
+ Same as :func:`mkdir`, but returns a legacy py path instance.
+ """
+ return legacy_path(self.mkdir(name))
+
+
+def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH:
+ """(deprecated) The file system path of the test module which collected this test."""
+ return legacy_path(self.path)
+
+
+def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH:
+ """The directory from which pytest was invoked.
+
+ Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(self.startpath)
+
+
+def Config_invocation_dir(self: Config) -> LEGACY_PATH:
+ """The directory from which pytest was invoked.
+
+ Prefer to use :attr:`invocation_params.dir `,
+ which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(str(self.invocation_params.dir))
+
+
+def Config_rootdir(self: Config) -> LEGACY_PATH:
+ """The path to the :ref:`rootdir `.
+
+ Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(str(self.rootpath))
+
+
+def Config_inifile(self: Config) -> LEGACY_PATH | None:
+ """The path to the :ref:`configfile `.
+
+ Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
+
+ :type: Optional[LEGACY_PATH]
+ """
+ return legacy_path(str(self.inipath)) if self.inipath else None
+
+
+def Session_startdir(self: Session) -> LEGACY_PATH:
+ """The path from which pytest was invoked.
+
+ Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
+
+ :type: LEGACY_PATH
+ """
+ return legacy_path(self.startpath)
+
+
+def Config__getini_unknown_type(self, name: str, type: str, value: str | list[str]):
+ if type == "pathlist":
+ # TODO: This assert is probably not valid in all cases.
+ assert self.inipath is not None
+ dp = self.inipath.parent
+ input_values = shlex.split(value) if isinstance(value, str) else value
+ return [legacy_path(str(dp / x)) for x in input_values]
+ else:
+ raise ValueError(f"unknown configuration type: {type}", value)
+
+
+def Node_fspath(self: Node) -> LEGACY_PATH:
+ """(deprecated) returns a legacy_path copy of self.path"""
+ return legacy_path(self.path)
+
+
+def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None:
+ self.path = Path(value)
+
+
+@hookimpl(tryfirst=True)
+def pytest_load_initial_conftests(early_config: Config) -> None:
+ """Monkeypatch legacy path attributes in several classes, as early as possible."""
+ mp = MonkeyPatch()
+ early_config.add_cleanup(mp.undo)
+
+ # Add Cache.makedir().
+ mp.setattr(Cache, "makedir", Cache_makedir, raising=False)
+
+ # Add FixtureRequest.fspath property.
+ mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False)
+
+ # Add TerminalReporter.startdir property.
+ mp.setattr(
+ TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False
+ )
+
+ # Add Config.{invocation_dir,rootdir,inifile} properties.
+ mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False)
+ mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False)
+ mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
+
+ # Add Session.startdir property.
+ mp.setattr(Session, "startdir", property(Session_startdir), raising=False)
+
+ # Add pathlist configuration type.
+ mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
+
+ # Add Node.fspath property.
+ mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
+
+
+@hookimpl
+def pytest_configure(config: Config) -> None:
+ """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed."""
+ if config.pluginmanager.has_plugin("tmpdir"):
+ mp = MonkeyPatch()
+ config.add_cleanup(mp.undo)
+ # Create TmpdirFactory and attach it to the config object.
+ #
+ # This is to comply with existing plugins which expect the handler to be
+ # available at pytest_configure time, but ideally should be moved entirely
+ # to the tmpdir_factory session fixture.
+ try:
+ tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined]
+ except AttributeError:
+ # tmpdir plugin is blocked.
+ pass
+ else:
+ _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True)
+ mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False)
+
+ config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir")
+
+
+@hookimpl
+def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None:
+ # pytester is not loaded by default and is commonly loaded from a conftest,
+ # so checking for it in `pytest_configure` is not enough.
+ is_pytester = plugin is manager.get_plugin("pytester")
+ if is_pytester and not manager.is_registered(LegacyTestdirPlugin):
+ manager.register(LegacyTestdirPlugin, "legacypath-pytester")
diff --git a/venv/lib/python3.11/site-packages/_pytest/logging.py b/venv/lib/python3.11/site-packages/_pytest/logging.py
new file mode 100644
index 000000000..3204d43ee
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/logging.py
@@ -0,0 +1,975 @@
+# mypy: allow-untyped-defs
+"""Access and control log capturing."""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+from collections.abc import Mapping
+from collections.abc import Set as AbstractSet
+from contextlib import contextmanager
+from contextlib import nullcontext
+from datetime import datetime
+from datetime import timedelta
+from datetime import timezone
+import io
+from io import StringIO
+import logging
+from logging import LogRecord
+import os
+from pathlib import Path
+import re
+from types import TracebackType
+from typing import final
+from typing import Generic
+from typing import Literal
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+from _pytest import nodes
+from _pytest._io import TerminalWriter
+from _pytest.capture import CaptureManager
+from _pytest.config import _strtobool
+from _pytest.config import Config
+from _pytest.config import create_terminal_writer
+from _pytest.config import hookimpl
+from _pytest.config import UsageError
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+
+
+if TYPE_CHECKING:
+ logging_StreamHandler = logging.StreamHandler[StringIO]
+else:
+ logging_StreamHandler = logging.StreamHandler
+
+DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
+DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
+_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
+caplog_handler_key = StashKey["LogCaptureHandler"]()
+caplog_records_key = StashKey[dict[str, list[logging.LogRecord]]]()
+
+
+def _remove_ansi_escape_sequences(text: str) -> str:
+ return _ANSI_ESCAPE_SEQ.sub("", text)
+
+
+class DatetimeFormatter(logging.Formatter):
+ """A logging formatter which formats record with
+ :func:`datetime.datetime.strftime` formatter instead of
+ :func:`time.strftime` in case of microseconds in format string.
+ """
+
+ def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str:
+ if datefmt and "%f" in datefmt:
+ ct = self.converter(record.created)
+ tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
+ # Construct `datetime.datetime` object from `struct_time`
+ # and msecs information from `record`
+ # Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861).
+ dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz)
+ return dt.strftime(datefmt)
+ # Use `logging.Formatter` for non-microsecond formats
+ return super().formatTime(record, datefmt)
+
+
+class ColoredLevelFormatter(DatetimeFormatter):
+ """A logging formatter which colorizes the %(levelname)..s part of the
+ log format passed to __init__."""
+
+ LOGLEVEL_COLOROPTS: Mapping[int, AbstractSet[str]] = {
+ logging.CRITICAL: {"red"},
+ logging.ERROR: {"red", "bold"},
+ logging.WARNING: {"yellow"},
+ logging.WARN: {"yellow"},
+ logging.INFO: {"green"},
+ logging.DEBUG: {"purple"},
+ logging.NOTSET: set(),
+ }
+ LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*(?:\.\d+)?s)")
+
+ def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ self._terminalwriter = terminalwriter
+ self._original_fmt = self._style._fmt
+ self._level_to_fmt_mapping: dict[int, str] = {}
+
+ for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
+ self.add_color_level(level, *color_opts)
+
+ def add_color_level(self, level: int, *color_opts: str) -> None:
+ """Add or update color opts for a log level.
+
+ :param level:
+ Log level to apply a style to, e.g. ``logging.INFO``.
+ :param color_opts:
+ ANSI escape sequence color options. Capitalized colors indicates
+ background color, i.e. ``'green', 'Yellow', 'bold'`` will give bold
+ green text on yellow background.
+
+ .. warning::
+ This is an experimental API.
+ """
+ assert self._fmt is not None
+ levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
+ if not levelname_fmt_match:
+ return
+ levelname_fmt = levelname_fmt_match.group()
+
+ formatted_levelname = levelname_fmt % {"levelname": logging.getLevelName(level)}
+
+ # add ANSI escape sequences around the formatted levelname
+ color_kwargs = {name: True for name in color_opts}
+ colorized_formatted_levelname = self._terminalwriter.markup(
+ formatted_levelname, **color_kwargs
+ )
+ self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
+ colorized_formatted_levelname, self._fmt
+ )
+
+ def format(self, record: logging.LogRecord) -> str:
+ fmt = self._level_to_fmt_mapping.get(record.levelno, self._original_fmt)
+ self._style._fmt = fmt
+ return super().format(record)
+
+
+class PercentStyleMultiline(logging.PercentStyle):
+ """A logging style with special support for multiline messages.
+
+ If the message of a record consists of multiple lines, this style
+ formats the message as if each line were logged separately.
+ """
+
+ def __init__(self, fmt: str, auto_indent: int | str | bool | None) -> None:
+ super().__init__(fmt)
+ self._auto_indent = self._get_auto_indent(auto_indent)
+
+ @staticmethod
+ def _get_auto_indent(auto_indent_option: int | str | bool | None) -> int:
+ """Determine the current auto indentation setting.
+
+ Specify auto indent behavior (on/off/fixed) by passing in
+ extra={"auto_indent": [value]} to the call to logging.log() or
+ using a --log-auto-indent [value] command line or the
+ log_auto_indent [value] config option.
+
+ Default behavior is auto-indent off.
+
+ Using the string "True" or "on" or the boolean True as the value
+ turns auto indent on, using the string "False" or "off" or the
+ boolean False or the int 0 turns it off, and specifying a
+ positive integer fixes the indentation position to the value
+ specified.
+
+ Any other values for the option are invalid, and will silently be
+ converted to the default.
+
+ :param None|bool|int|str auto_indent_option:
+ User specified option for indentation from command line, config
+ or extra kwarg. Accepts int, bool or str. str option accepts the
+ same range of values as boolean config options, as well as
+ positive integers represented in str form.
+
+ :returns:
+ Indentation value, which can be
+ -1 (automatically determine indentation) or
+ 0 (auto-indent turned off) or
+ >0 (explicitly set indentation position).
+ """
+ match auto_indent_option:
+ case None | False:
+ return 0
+ case True:
+ return -1
+ case int():
+ return auto_indent_option
+ case str():
+ try:
+ return int(auto_indent_option)
+ except ValueError:
+ pass
+ try:
+ if _strtobool(auto_indent_option):
+ return -1
+ except ValueError:
+ return 0
+ return 0
+
+ def format(self, record: logging.LogRecord) -> str:
+ if "\n" in record.message:
+ if hasattr(record, "auto_indent"):
+ # Passed in from the "extra={}" kwarg on the call to logging.log().
+ auto_indent = self._get_auto_indent(record.auto_indent)
+ else:
+ auto_indent = self._auto_indent
+
+ if auto_indent:
+ lines = record.message.splitlines()
+ formatted = self._fmt % {**record.__dict__, "message": lines[0]}
+
+ if auto_indent < 0:
+ indentation = _remove_ansi_escape_sequences(formatted).find(
+ lines[0]
+ )
+ else:
+ # Optimizes logging by allowing a fixed indentation.
+ indentation = auto_indent
+ lines[0] = formatted
+ return ("\n" + " " * indentation).join(lines)
+ return self._fmt % record.__dict__
+
+
+def get_option_ini(config: Config, *names: str):
+ for name in names:
+ ret = config.getoption(name) # 'default' arg won't work as expected
+ if ret is None:
+ ret = config.getini(name)
+ if ret:
+ return ret
+
+
+def pytest_addoption(parser: Parser) -> None:
+ """Add options to control log capturing."""
+ group = parser.getgroup("logging")
+
+ def add_option_ini(option, dest, default=None, type=None, **kwargs):
+ parser.addini(
+ dest, default=default, type=type, help="Default value for " + option
+ )
+ group.addoption(option, dest=dest, **kwargs)
+
+ add_option_ini(
+ "--log-level",
+ dest="log_level",
+ default=None,
+ metavar="LEVEL",
+ help=(
+ "Level of messages to catch/display."
+ " Not set by default, so it depends on the root/parent log handler's"
+ ' effective level, where it is "WARNING" by default.'
+ ),
+ )
+ add_option_ini(
+ "--log-format",
+ dest="log_format",
+ default=DEFAULT_LOG_FORMAT,
+ help="Log format used by the logging module",
+ )
+ add_option_ini(
+ "--log-date-format",
+ dest="log_date_format",
+ default=DEFAULT_LOG_DATE_FORMAT,
+ help="Log date format used by the logging module",
+ )
+ parser.addini(
+ "log_cli",
+ default=False,
+ type="bool",
+ help='Enable log display during test run (also known as "live logging")',
+ )
+ add_option_ini(
+ "--log-cli-level", dest="log_cli_level", default=None, help="CLI logging level"
+ )
+ add_option_ini(
+ "--log-cli-format",
+ dest="log_cli_format",
+ default=None,
+ help="Log format used by the logging module",
+ )
+ add_option_ini(
+ "--log-cli-date-format",
+ dest="log_cli_date_format",
+ default=None,
+ help="Log date format used by the logging module",
+ )
+ add_option_ini(
+ "--log-file",
+ dest="log_file",
+ default=None,
+ help="Path to a file when logging will be written to",
+ )
+ add_option_ini(
+ "--log-file-mode",
+ dest="log_file_mode",
+ default="w",
+ choices=["w", "a"],
+ help="Log file open mode",
+ )
+ add_option_ini(
+ "--log-file-level",
+ dest="log_file_level",
+ default=None,
+ help="Log file logging level",
+ )
+ add_option_ini(
+ "--log-file-format",
+ dest="log_file_format",
+ default=None,
+ help="Log format used by the logging module",
+ )
+ add_option_ini(
+ "--log-file-date-format",
+ dest="log_file_date_format",
+ default=None,
+ help="Log date format used by the logging module",
+ )
+ add_option_ini(
+ "--log-auto-indent",
+ dest="log_auto_indent",
+ default=None,
+ help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
+ )
+ group.addoption(
+ "--log-disable",
+ action="append",
+ default=[],
+ dest="logger_disable",
+ help="Disable a logger by name. Can be passed multiple times.",
+ )
+
+
+_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
+
+
+# Not using @contextmanager for performance reasons.
+class catching_logs(Generic[_HandlerType]):
+ """Context manager that prepares the whole logging machinery properly."""
+
+ __slots__ = ("attached_loggers", "handler", "level", "orig_level")
+
+ def __init__(self, handler: _HandlerType, level: int | None = None) -> None:
+ self.handler = handler
+ self.level = level
+ self.attached_loggers: list[logging.Logger] = []
+
+ def __enter__(self) -> _HandlerType:
+ root_logger = logging.getLogger()
+ if self.level is not None:
+ self.handler.setLevel(self.level)
+ # Attach to root logger.
+ root_logger.addHandler(self.handler)
+ self.attached_loggers.append(root_logger)
+ # Attach to all non-propagating loggers (won't reach root).
+ # Note that will miss loggers that *become* non-propagating
+ # after the `__enter__`. Not worth the trouble for now.
+ for logger in root_logger.manager.loggerDict.values():
+ if (
+ isinstance(logger, logging.Logger)
+ and not logger.propagate
+ and logger is not root_logger
+ ):
+ logger.addHandler(self.handler)
+ self.attached_loggers.append(logger)
+ if self.level is not None:
+ # Non-propagating loggers still inherit the level (unless a logger
+ # explicitly set level), so only do this on the root logger.
+ self.orig_level = root_logger.level
+ root_logger.setLevel(min(self.orig_level, self.level))
+ return self.handler
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ root_logger = logging.getLogger()
+ if self.level is not None:
+ root_logger.setLevel(self.orig_level)
+ for logger in self.attached_loggers:
+ logger.removeHandler(self.handler)
+ self.attached_loggers.clear()
+
+
+class LogCaptureHandler(logging_StreamHandler):
+ """A logging handler that stores log records and the log text."""
+
+ def __init__(self) -> None:
+ """Create a new log handler."""
+ super().__init__(StringIO())
+ self.records: list[logging.LogRecord] = []
+
+ def emit(self, record: logging.LogRecord) -> None:
+ """Keep the log records in a list in addition to the log text."""
+ self.records.append(record)
+ super().emit(record)
+
+ def reset(self) -> None:
+ self.records = []
+ self.stream = StringIO()
+
+ def clear(self) -> None:
+ self.records.clear()
+ self.stream = StringIO()
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ if logging.raiseExceptions:
+ # Fail the test if the log message is bad (emit failed).
+ # The default behavior of logging is to print "Logging error"
+ # to stderr with the call stack and some extra details.
+ # pytest wants to make such mistakes visible during testing.
+ raise # noqa: PLE0704
+
+
+@final
+class LogCaptureFixture:
+ """Provides access and control of log capturing."""
+
+ def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._item = item
+ self._initial_handler_level: int | None = None
+ # Dict of log name -> log level.
+ self._initial_logger_levels: dict[str | None, int] = {}
+ self._initial_disabled_logging_level: int | None = None
+
+ def _finalize(self) -> None:
+ """Finalize the fixture.
+
+ This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
+ """
+ # Restore log levels.
+ if self._initial_handler_level is not None:
+ self.handler.setLevel(self._initial_handler_level)
+ for logger_name, level in self._initial_logger_levels.items():
+ logger = logging.getLogger(logger_name)
+ logger.setLevel(level)
+ # Disable logging at the original disabled logging level.
+ if self._initial_disabled_logging_level is not None:
+ logging.disable(self._initial_disabled_logging_level)
+ self._initial_disabled_logging_level = None
+
+ @property
+ def handler(self) -> LogCaptureHandler:
+ """Get the logging handler used by the fixture."""
+ return self._item.stash[caplog_handler_key]
+
+ def get_records(
+ self, when: Literal["setup", "call", "teardown"]
+ ) -> list[logging.LogRecord]:
+ """Get the logging records for one of the possible test phases.
+
+ :param when:
+ Which test phase to obtain the records from.
+ Valid values are: "setup", "call" and "teardown".
+
+ :returns: The list of captured records at the given stage.
+
+ .. versionadded:: 3.4
+ """
+ return self._item.stash[caplog_records_key].get(when, [])
+
+ @property
+ def text(self) -> str:
+ """The formatted log text."""
+ return _remove_ansi_escape_sequences(self.handler.stream.getvalue())
+
+ @property
+ def records(self) -> list[logging.LogRecord]:
+ """The list of log records."""
+ return self.handler.records
+
+ @property
+ def record_tuples(self) -> list[tuple[str, int, str]]:
+ """A list of a stripped down version of log records intended
+ for use in assertion comparison.
+
+ The format of the tuple is:
+
+ (logger_name, log_level, message)
+ """
+ return [(r.name, r.levelno, r.getMessage()) for r in self.records]
+
+ @property
+ def messages(self) -> list[str]:
+ """A list of format-interpolated log messages.
+
+ Unlike 'records', which contains the format string and parameters for
+ interpolation, log messages in this list are all interpolated.
+
+ Unlike 'text', which contains the output from the handler, log
+ messages in this list are unadorned with levels, timestamps, etc,
+ making exact comparisons more reliable.
+
+ Note that traceback or stack info (from :func:`logging.exception` or
+ the `exc_info` or `stack_info` arguments to the logging functions) is
+ not included, as this is added by the formatter in the handler.
+
+ .. versionadded:: 3.7
+ """
+ return [r.getMessage() for r in self.records]
+
+ def clear(self) -> None:
+ """Reset the list of log records and the captured log text."""
+ self.handler.clear()
+
+ def _force_enable_logging(
+ self, level: int | str, logger_obj: logging.Logger
+ ) -> int:
+ """Enable the desired logging level if the global level was disabled via ``logging.disabled``.
+
+ Only enables logging levels greater than or equal to the requested ``level``.
+
+ Does nothing if the desired ``level`` wasn't disabled.
+
+ :param level:
+ The logger level caplog should capture.
+ All logging is enabled if a non-standard logging level string is supplied.
+ Valid level strings are in :data:`logging._nameToLevel`.
+ :param logger_obj: The logger object to check.
+
+ :return: The original disabled logging level.
+ """
+ original_disable_level: int = logger_obj.manager.disable
+
+ if isinstance(level, str):
+ # Try to translate the level string to an int for `logging.disable()`
+ level = logging.getLevelName(level) # type: ignore[deprecated]
+
+ if not isinstance(level, int):
+ # The level provided was not valid, so just un-disable all logging.
+ logging.disable(logging.NOTSET)
+ elif not logger_obj.isEnabledFor(level):
+ # Each level is `10` away from other levels.
+ # https://docs.python.org/3/library/logging.html#logging-levels
+ disable_level = max(level - 10, logging.NOTSET)
+ logging.disable(disable_level)
+
+ return original_disable_level
+
+ def set_level(self, level: int | str, logger: str | None = None) -> None:
+ """Set the threshold level of a logger for the duration of a test.
+
+ Logging messages which are less severe than this level will not be captured.
+
+ .. versionchanged:: 3.4
+ The levels of the loggers changed by this function will be
+ restored to their initial values at the end of the test.
+
+ Will enable the requested logging level if it was disabled via :func:`logging.disable`.
+
+ :param level: The level.
+ :param logger: The logger to update. If not given, the root logger.
+ """
+ logger_obj = logging.getLogger(logger)
+ # Save the original log-level to restore it during teardown.
+ self._initial_logger_levels.setdefault(logger, logger_obj.level)
+ logger_obj.setLevel(level)
+ if self._initial_handler_level is None:
+ self._initial_handler_level = self.handler.level
+ self.handler.setLevel(level)
+ initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
+ if self._initial_disabled_logging_level is None:
+ self._initial_disabled_logging_level = initial_disabled_logging_level
+
+ @contextmanager
+ def at_level(self, level: int | str, logger: str | None = None) -> Generator[None]:
+ """Context manager that sets the level for capturing of logs. After
+ the end of the 'with' statement the level is restored to its original
+ value.
+
+ Will enable the requested logging level if it was disabled via :func:`logging.disable`.
+
+ :param level: The level.
+ :param logger: The logger to update. If not given, the root logger.
+ """
+ logger_obj = logging.getLogger(logger)
+ orig_level = logger_obj.level
+ logger_obj.setLevel(level)
+ handler_orig_level = self.handler.level
+ self.handler.setLevel(level)
+ original_disable_level = self._force_enable_logging(level, logger_obj)
+ try:
+ yield
+ finally:
+ logger_obj.setLevel(orig_level)
+ self.handler.setLevel(handler_orig_level)
+ logging.disable(original_disable_level)
+
+ @contextmanager
+ def filtering(self, filter_: logging.Filter) -> Generator[None]:
+ """Context manager that temporarily adds the given filter to the caplog's
+ :meth:`handler` for the 'with' statement block, and removes that filter at the
+ end of the block.
+
+ :param filter_: A custom :class:`logging.Filter` object.
+
+ .. versionadded:: 7.5
+ """
+ self.handler.addFilter(filter_)
+ try:
+ yield
+ finally:
+ self.handler.removeFilter(filter_)
+
+
+@fixture
+def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture]:
+ """Access and control log capturing.
+
+ Captured logs are available through the following properties/methods::
+
+ * caplog.messages -> list of format-interpolated log messages
+ * caplog.text -> string containing formatted log output
+ * caplog.records -> list of logging.LogRecord instances
+ * caplog.record_tuples -> list of (logger_name, level, message) tuples
+ * caplog.clear() -> clear captured records and formatted log output string
+ """
+ result = LogCaptureFixture(request.node, _ispytest=True)
+ yield result
+ result._finalize()
+
+
+def get_log_level_for_setting(config: Config, *setting_names: str) -> int | None:
+ for setting_name in setting_names:
+ log_level = config.getoption(setting_name)
+ if log_level is None:
+ log_level = config.getini(setting_name)
+ if log_level:
+ break
+ else:
+ return None
+
+ if isinstance(log_level, str):
+ log_level = log_level.upper()
+ try:
+ return int(getattr(logging, log_level, log_level))
+ except ValueError as e:
+ # Python logging does not recognise this as a logging level
+ raise UsageError(
+ f"'{log_level}' is not recognized as a logging level name for "
+ f"'{setting_name}'. Please consider passing the "
+ "logging level num instead."
+ ) from e
+
+
+# run after terminalreporter/capturemanager are configured
+@hookimpl(trylast=True)
+def pytest_configure(config: Config) -> None:
+ config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
+
+
+class LoggingPlugin:
+ """Attaches to the logging module and captures log messages for each test."""
+
+ def __init__(self, config: Config) -> None:
+ """Create a new plugin to capture log messages.
+
+ The formatter can be safely shared across all handlers so
+ create a single one for the entire test session here.
+ """
+ self._config = config
+
+ # Report logging.
+ self.formatter = self._create_formatter(
+ get_option_ini(config, "log_format"),
+ get_option_ini(config, "log_date_format"),
+ get_option_ini(config, "log_auto_indent"),
+ )
+ self.log_level = get_log_level_for_setting(config, "log_level")
+ self.caplog_handler = LogCaptureHandler()
+ self.caplog_handler.setFormatter(self.formatter)
+ self.report_handler = LogCaptureHandler()
+ self.report_handler.setFormatter(self.formatter)
+
+ # File logging.
+ self.log_file_level = get_log_level_for_setting(
+ config, "log_file_level", "log_level"
+ )
+ log_file = get_option_ini(config, "log_file") or os.devnull
+ if log_file != os.devnull:
+ directory = os.path.dirname(os.path.abspath(log_file))
+ if not os.path.isdir(directory):
+ os.makedirs(directory)
+
+ self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
+ self.log_file_handler = _FileHandler(
+ log_file, mode=self.log_file_mode, encoding="UTF-8"
+ )
+ log_file_format = get_option_ini(config, "log_file_format", "log_format")
+ log_file_date_format = get_option_ini(
+ config, "log_file_date_format", "log_date_format"
+ )
+
+ log_file_formatter = DatetimeFormatter(
+ log_file_format, datefmt=log_file_date_format
+ )
+ self.log_file_handler.setFormatter(log_file_formatter)
+
+ # CLI/live logging.
+ self.log_cli_level = get_log_level_for_setting(
+ config, "log_cli_level", "log_level"
+ )
+ if self._log_cli_enabled():
+ terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
+ # Guaranteed by `_log_cli_enabled()`.
+ assert terminal_reporter is not None
+ capture_manager = config.pluginmanager.get_plugin("capturemanager")
+ # if capturemanager plugin is disabled, live logging still works.
+ self.log_cli_handler: (
+ _LiveLoggingStreamHandler | _LiveLoggingNullHandler
+ ) = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
+ else:
+ self.log_cli_handler = _LiveLoggingNullHandler()
+ log_cli_formatter = self._create_formatter(
+ get_option_ini(config, "log_cli_format", "log_format"),
+ get_option_ini(config, "log_cli_date_format", "log_date_format"),
+ get_option_ini(config, "log_auto_indent"),
+ )
+ self.log_cli_handler.setFormatter(log_cli_formatter)
+ self._disable_loggers(loggers_to_disable=config.option.logger_disable)
+
+ def _disable_loggers(self, loggers_to_disable: list[str]) -> None:
+ if not loggers_to_disable:
+ return
+
+ for name in loggers_to_disable:
+ logger = logging.getLogger(name)
+ logger.disabled = True
+
+ def _create_formatter(self, log_format, log_date_format, auto_indent):
+ # Color option doesn't exist if terminal plugin is disabled.
+ color = getattr(self._config.option, "color", "no")
+ if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(
+ log_format
+ ):
+ formatter: logging.Formatter = ColoredLevelFormatter(
+ create_terminal_writer(self._config), log_format, log_date_format
+ )
+ else:
+ formatter = DatetimeFormatter(log_format, log_date_format)
+
+ formatter._style = PercentStyleMultiline(
+ formatter._style._fmt, auto_indent=auto_indent
+ )
+
+ return formatter
+
+ def set_log_path(self, fname: str) -> None:
+ """Set the filename parameter for Logging.FileHandler().
+
+ Creates parent directory if it does not exist.
+
+ .. warning::
+ This is an experimental API.
+ """
+ fpath = Path(fname)
+
+ if not fpath.is_absolute():
+ fpath = self._config.rootpath / fpath
+
+ if not fpath.parent.exists():
+ fpath.parent.mkdir(exist_ok=True, parents=True)
+
+ # https://github.com/python/mypy/issues/11193
+ stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
+ old_stream = self.log_file_handler.setStream(stream)
+ if old_stream:
+ old_stream.close()
+
+ def _log_cli_enabled(self) -> bool:
+ """Return whether live logging is enabled."""
+ enabled = self._config.getoption(
+ "--log-cli-level"
+ ) is not None or self._config.getini("log_cli")
+ if not enabled:
+ return False
+
+ terminal_reporter = self._config.pluginmanager.get_plugin("terminalreporter")
+ if terminal_reporter is None:
+ # terminal reporter is disabled e.g. by pytest-xdist.
+ return False
+
+ return True
+
+ @hookimpl(wrapper=True, tryfirst=True)
+ def pytest_sessionstart(self) -> Generator[None]:
+ self.log_cli_handler.set_when("sessionstart")
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ return (yield)
+
+ @hookimpl(wrapper=True, tryfirst=True)
+ def pytest_collection(self) -> Generator[None]:
+ self.log_cli_handler.set_when("collection")
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ return (yield)
+
+ @hookimpl(wrapper=True)
+ def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]:
+ if session.config.option.collectonly:
+ return (yield)
+
+ if self._log_cli_enabled() and self._config.get_verbosity() < 1:
+ # The verbose flag is needed to avoid messy test progress output.
+ self._config.option.verbose = 1
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ return (yield) # Run all the tests.
+
+ @hookimpl
+ def pytest_runtest_logstart(self) -> None:
+ self.log_cli_handler.reset()
+ self.log_cli_handler.set_when("start")
+
+ @hookimpl
+ def pytest_runtest_logreport(self) -> None:
+ self.log_cli_handler.set_when("logreport")
+
+ @contextmanager
+ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None]:
+ """Implement the internals of the pytest_runtest_xxx() hooks."""
+ with (
+ catching_logs(
+ self.caplog_handler,
+ level=self.log_level,
+ ) as caplog_handler,
+ catching_logs(
+ self.report_handler,
+ level=self.log_level,
+ ) as report_handler,
+ ):
+ caplog_handler.reset()
+ report_handler.reset()
+ item.stash[caplog_records_key][when] = caplog_handler.records
+ item.stash[caplog_handler_key] = caplog_handler
+
+ try:
+ yield
+ finally:
+ log = report_handler.stream.getvalue().strip()
+ item.add_report_section(when, "log", log)
+
+ @hookimpl(wrapper=True)
+ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None]:
+ self.log_cli_handler.set_when("setup")
+
+ empty: dict[str, list[logging.LogRecord]] = {}
+ item.stash[caplog_records_key] = empty
+ with self._runtest_for(item, "setup"):
+ yield
+
+ @hookimpl(wrapper=True)
+ def pytest_runtest_call(self, item: nodes.Item) -> Generator[None]:
+ self.log_cli_handler.set_when("call")
+
+ with self._runtest_for(item, "call"):
+ yield
+
+ @hookimpl(wrapper=True)
+ def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None]:
+ self.log_cli_handler.set_when("teardown")
+
+ try:
+ with self._runtest_for(item, "teardown"):
+ yield
+ finally:
+ del item.stash[caplog_records_key]
+ del item.stash[caplog_handler_key]
+
+ @hookimpl
+ def pytest_runtest_logfinish(self) -> None:
+ self.log_cli_handler.set_when("finish")
+
+ @hookimpl(wrapper=True, tryfirst=True)
+ def pytest_sessionfinish(self) -> Generator[None]:
+ self.log_cli_handler.set_when("sessionfinish")
+
+ with catching_logs(self.log_cli_handler, level=self.log_cli_level):
+ with catching_logs(self.log_file_handler, level=self.log_file_level):
+ return (yield)
+
+ @hookimpl
+ def pytest_unconfigure(self) -> None:
+ # Close the FileHandler explicitly.
+ # (logging.shutdown might have lost the weakref?!)
+ self.log_file_handler.close()
+
+
+class _FileHandler(logging.FileHandler):
+ """A logging FileHandler with pytest tweaks."""
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
+
+
+class _LiveLoggingStreamHandler(logging_StreamHandler):
+ """A logging StreamHandler used by the live logging feature: it will
+ write a newline before the first log message in each test.
+
+ During live logging we must also explicitly disable stdout/stderr
+ capturing otherwise it will get captured and won't appear in the
+ terminal.
+ """
+
+ # Officially stream needs to be a IO[str], but TerminalReporter
+ # isn't. So force it.
+ stream: TerminalReporter = None # type: ignore
+
+ def __init__(
+ self,
+ terminal_reporter: TerminalReporter,
+ capture_manager: CaptureManager | None,
+ ) -> None:
+ super().__init__(stream=terminal_reporter) # type: ignore[arg-type]
+ self.capture_manager = capture_manager
+ self.reset()
+ self.set_when(None)
+ self._test_outcome_written = False
+
+ def reset(self) -> None:
+ """Reset the handler; should be called before the start of each test."""
+ self._first_record_emitted = False
+
+ def set_when(self, when: str | None) -> None:
+ """Prepare for the given test phase (setup/call/teardown)."""
+ self._when = when
+ self._section_name_shown = False
+ if when == "start":
+ self._test_outcome_written = False
+
+ def emit(self, record: logging.LogRecord) -> None:
+ ctx_manager = (
+ self.capture_manager.global_and_fixture_disabled()
+ if self.capture_manager
+ else nullcontext()
+ )
+ with ctx_manager:
+ if not self._first_record_emitted:
+ self.stream.write("\n")
+ self._first_record_emitted = True
+ elif self._when in ("teardown", "finish"):
+ if not self._test_outcome_written:
+ self._test_outcome_written = True
+ self.stream.write("\n")
+ if not self._section_name_shown and self._when:
+ self.stream.section("live log " + self._when, sep="-", bold=True)
+ self._section_name_shown = True
+ super().emit(record)
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
+
+
+class _LiveLoggingNullHandler(logging.NullHandler):
+ """A logging handler used when live logging is disabled."""
+
+ def reset(self) -> None:
+ pass
+
+ def set_when(self, when: str) -> None:
+ pass
+
+ def handleError(self, record: logging.LogRecord) -> None:
+ # Handled by LogCaptureHandler.
+ pass
diff --git a/venv/lib/python3.11/site-packages/_pytest/main.py b/venv/lib/python3.11/site-packages/_pytest/main.py
new file mode 100644
index 000000000..c4df4e469
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/main.py
@@ -0,0 +1,1215 @@
+"""Core implementation of the testing process: init, session, runtest loop."""
+
+from __future__ import annotations
+
+import argparse
+from collections.abc import Callable
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Sequence
+from collections.abc import Set as AbstractSet
+import dataclasses
+import fnmatch
+import functools
+import importlib
+import importlib.util
+import os
+from pathlib import Path
+import sys
+from typing import final
+from typing import Literal
+from typing import overload
+from typing import TYPE_CHECKING
+import warnings
+
+import pluggy
+
+from _pytest import nodes
+import _pytest._code
+from _pytest.config import Config
+from _pytest.config import directory_arg
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import PytestPluginManager
+from _pytest.config import UsageError
+from _pytest.config.argparsing import OverrideIniAction
+from _pytest.config.argparsing import Parser
+from _pytest.outcomes import exit
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import fnmatch_ex
+from _pytest.pathlib import safe_exists
+from _pytest.pathlib import samefile_nofollow
+from _pytest.pathlib import scandir
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+from _pytest.runner import collect_one_node
+from _pytest.runner import SetupState
+from _pytest.warning_types import PytestWarning
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+ from _pytest.fixtures import FixtureManager
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption( # private to use reserved lower-case short option
+ "-x",
+ "--exitfirst",
+ action="store_const",
+ dest="maxfail",
+ const=1,
+ help="Exit instantly on first error or failed test",
+ )
+ group.addoption(
+ "--maxfail",
+ metavar="num",
+ action="store",
+ type=int,
+ dest="maxfail",
+ default=0,
+ help="Exit after first num failures or errors",
+ )
+ group.addoption(
+ "--strict-config",
+ action=OverrideIniAction,
+ ini_option="strict_config",
+ ini_value="true",
+ help="Enables the strict_config option",
+ )
+ group.addoption(
+ "--strict-markers",
+ action=OverrideIniAction,
+ ini_option="strict_markers",
+ ini_value="true",
+ help="Enables the strict_markers option",
+ )
+ group.addoption(
+ "--strict",
+ action=OverrideIniAction,
+ ini_option="strict",
+ ini_value="true",
+ help="Enables the strict option",
+ )
+ parser.addini(
+ "strict_config",
+ "Any warnings encountered while parsing the `pytest` section of the "
+ "configuration file raise errors",
+ type="bool",
+ # None => fallback to `strict`.
+ default=None,
+ )
+ parser.addini(
+ "strict_markers",
+ "Markers not registered in the `markers` section of the configuration "
+ "file raise errors",
+ type="bool",
+ # None => fallback to `strict`.
+ default=None,
+ )
+ parser.addini(
+ "strict",
+ "Enables all strictness options, currently: "
+ "strict_config, strict_markers, strict_xfail, strict_parametrization_ids",
+ type="bool",
+ default=False,
+ )
+
+ group = parser.getgroup("pytest-warnings")
+ group.addoption(
+ "-W",
+ "--pythonwarnings",
+ action="append",
+ help="Set which warnings to report, see -W option of Python itself",
+ )
+ group.addoption(
+ "--max-warnings",
+ action="store",
+ type=int,
+ default=None,
+ metavar="num",
+ dest="max_warnings",
+ help="Exit with error if all tests pass but the number of warnings exceeds this threshold",
+ )
+ parser.addini(
+ "filterwarnings",
+ type="linelist",
+ help="Each line specifies a pattern for "
+ "warnings.filterwarnings. "
+ "Processed after -W/--pythonwarnings.",
+ )
+ parser.addini(
+ "max_warnings",
+ help="Exit with error if all tests pass but the number of warnings exceeds this threshold",
+ )
+
+ group = parser.getgroup("collect", "collection")
+ group.addoption(
+ "--collectonly",
+ "--collect-only",
+ "--co",
+ action="store_true",
+ help="Only collect tests, don't execute them",
+ )
+ group.addoption(
+ "--pyargs",
+ action="store_true",
+ help="Try to interpret all arguments as Python packages",
+ )
+ group.addoption(
+ "--ignore",
+ action="append",
+ metavar="path",
+ help="Ignore path during collection (multi-allowed)",
+ )
+ group.addoption(
+ "--ignore-glob",
+ action="append",
+ metavar="path",
+ help="Ignore path pattern during collection (multi-allowed)",
+ )
+ group.addoption(
+ "--deselect",
+ action="append",
+ metavar="nodeid_prefix",
+ help="Deselect item (via node id prefix) during collection (multi-allowed)",
+ )
+ group.addoption(
+ "--confcutdir",
+ dest="confcutdir",
+ default=None,
+ metavar="dir",
+ type=functools.partial(directory_arg, optname="--confcutdir"),
+ help="Only load conftest.py's relative to specified dir",
+ )
+ group.addoption(
+ "--noconftest",
+ action="store_true",
+ dest="noconftest",
+ default=False,
+ help="Don't load any conftest.py files",
+ )
+ group.addoption(
+ "--keepduplicates",
+ "--keep-duplicates",
+ action="store_true",
+ dest="keepduplicates",
+ default=False,
+ help="Keep duplicate tests",
+ )
+ group.addoption(
+ "--collect-in-virtualenv",
+ action="store_true",
+ dest="collect_in_virtualenv",
+ default=False,
+ help="Don't ignore tests in a local virtualenv directory",
+ )
+ group.addoption(
+ "--continue-on-collection-errors",
+ action="store_true",
+ default=False,
+ dest="continue_on_collection_errors",
+ help="Force test execution even if collection errors occur",
+ )
+ group.addoption(
+ "--import-mode",
+ default="prepend",
+ choices=["prepend", "append", "importlib"],
+ dest="importmode",
+ help="Prepend/append to sys.path when importing test modules and conftest "
+ "files. Default: prepend.",
+ )
+ parser.addini(
+ "norecursedirs",
+ "Directory patterns to avoid for recursion",
+ type="args",
+ default=[
+ "*.egg",
+ ".*",
+ "_darcs",
+ "build",
+ "CVS",
+ "dist",
+ "node_modules",
+ "venv",
+ "{arch}",
+ ],
+ )
+ parser.addini(
+ "testpaths",
+ "Directories to search for tests when no files or directories are given on the "
+ "command line",
+ type="args",
+ default=[],
+ )
+ parser.addini(
+ "collect_imported_tests",
+ "Whether to collect tests in imported modules outside `testpaths`",
+ type="bool",
+ default=True,
+ )
+ parser.addini(
+ "consider_namespace_packages",
+ type="bool",
+ default=False,
+ help="Consider namespace packages when resolving module names during import",
+ )
+
+ group = parser.getgroup("debugconfig", "test session debugging and configuration")
+ group._addoption( # private to use reserved lower-case short option
+ "-c",
+ "--config-file",
+ metavar="FILE",
+ type=str,
+ dest="inifilename",
+ help="Load configuration from `FILE` instead of trying to locate one of the "
+ "implicit configuration files.",
+ )
+ group.addoption(
+ "--rootdir",
+ action="store",
+ dest="rootdir",
+ help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', "
+ "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: "
+ "'$HOME/root_dir'.",
+ )
+ group.addoption(
+ "--basetemp",
+ dest="basetemp",
+ default=None,
+ type=validate_basetemp,
+ metavar="dir",
+ help=(
+ "Base temporary directory for this test run. "
+ "(Warning: this directory is removed if it exists.)"
+ ),
+ )
+
+
+def validate_basetemp(path: str) -> str:
+ # GH 7119
+ msg = "basetemp must not be empty, the current working directory or any parent directory of it"
+
+ # empty path
+ if not path:
+ raise argparse.ArgumentTypeError(msg)
+
+ def is_ancestor(base: Path, query: Path) -> bool:
+ """Return whether query is an ancestor of base."""
+ if base == query:
+ return True
+ return query in base.parents
+
+ # check if path is an ancestor of cwd
+ if is_ancestor(Path.cwd(), Path(path).absolute()):
+ raise argparse.ArgumentTypeError(msg)
+
+ # check symlinks for ancestors
+ if is_ancestor(Path.cwd().resolve(), Path(path).resolve()):
+ raise argparse.ArgumentTypeError(msg)
+
+ return path
+
+
+def wrap_session(
+ config: Config, doit: Callable[[Config, Session], int | ExitCode | None]
+) -> int | ExitCode:
+ """Skeleton command line program."""
+ session = Session.from_config(config)
+ session.exitstatus = ExitCode.OK
+ initstate = 0
+ try:
+ try:
+ config._do_configure()
+ initstate = 1
+ config.hook.pytest_sessionstart(session=session)
+ initstate = 2
+ session.exitstatus = doit(config, session) or 0
+ except UsageError:
+ session.exitstatus = ExitCode.USAGE_ERROR
+ raise
+ except Failed:
+ session.exitstatus = ExitCode.TESTS_FAILED
+ except (KeyboardInterrupt, exit.Exception):
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ exitstatus: int | ExitCode = ExitCode.INTERRUPTED
+ if isinstance(excinfo.value, exit.Exception):
+ if excinfo.value.returncode is not None:
+ exitstatus = excinfo.value.returncode
+ if initstate < 2:
+ sys.stderr.write(f"{excinfo.typename}: {excinfo.value.msg}\n")
+ config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
+ session.exitstatus = exitstatus
+ except BaseException:
+ session.exitstatus = ExitCode.INTERNAL_ERROR
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ try:
+ config.notify_exception(excinfo, config.option)
+ except exit.Exception as exc:
+ if exc.returncode is not None:
+ session.exitstatus = exc.returncode
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
+ else:
+ if isinstance(excinfo.value, SystemExit):
+ sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
+
+ finally:
+ # Explicitly break reference cycle.
+ excinfo = None # type: ignore
+ os.chdir(session.startpath)
+ if initstate >= 2:
+ try:
+ config.hook.pytest_sessionfinish(
+ session=session, exitstatus=session.exitstatus
+ )
+ except exit.Exception as exc:
+ if exc.returncode is not None:
+ session.exitstatus = exc.returncode
+ sys.stderr.write(f"{type(exc).__name__}: {exc}\n")
+ config._ensure_unconfigure()
+ return session.exitstatus
+
+
+def pytest_cmdline_main(config: Config) -> int | ExitCode:
+ return wrap_session(config, _main)
+
+
+def _main(config: Config, session: Session) -> int | ExitCode | None:
+ """Default command line protocol for initialization, session,
+ running tests and reporting."""
+ config.hook.pytest_collection(session=session)
+ config.hook.pytest_runtestloop(session=session)
+
+ if session.testsfailed:
+ return ExitCode.TESTS_FAILED
+ elif session.testscollected == 0:
+ return ExitCode.NO_TESTS_COLLECTED
+ return None
+
+
+def pytest_collection(session: Session) -> None:
+ session.perform_collect()
+
+
+def pytest_runtestloop(session: Session) -> bool:
+ if session.testsfailed and not session.config.option.continue_on_collection_errors:
+ raise session.Interrupted(
+ f"{session.testsfailed} error{'s' if session.testsfailed != 1 else ''} during collection"
+ )
+
+ if session.config.option.collectonly:
+ return True
+
+ for i, item in enumerate(session.items):
+ nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
+ item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
+ if session.shouldfail:
+ raise session.Failed(session.shouldfail)
+ if session.shouldstop:
+ raise session.Interrupted(session.shouldstop)
+ return True
+
+
+def _in_venv(path: Path) -> bool:
+ """Attempt to detect if ``path`` is the root of a Virtual Environment by
+ checking for the existence of the pyvenv.cfg file.
+
+ [https://peps.python.org/pep-0405/]
+
+ For regression protection we also check for conda environments that do not include pyenv.cfg yet --
+ https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg.
+
+ Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902.
+
+ """
+ try:
+ return (
+ path.joinpath("pyvenv.cfg").is_file()
+ or path.joinpath("conda-meta", "history").is_file()
+ )
+ except OSError:
+ return False
+
+
+def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None:
+ if collection_path.name == "__pycache__":
+ return True
+
+ ignore_paths = config._getconftest_pathlist(
+ "collect_ignore", path=collection_path.parent
+ )
+ ignore_paths = ignore_paths or []
+ excludeopt = config.getoption("ignore")
+ if excludeopt:
+ ignore_paths.extend(absolutepath(x) for x in excludeopt)
+
+ if collection_path in ignore_paths:
+ return True
+
+ ignore_globs = config._getconftest_pathlist(
+ "collect_ignore_glob", path=collection_path.parent
+ )
+ ignore_globs = ignore_globs or []
+ excludeglobopt = config.getoption("ignore_glob")
+ if excludeglobopt:
+ ignore_globs.extend(absolutepath(x) for x in excludeglobopt)
+
+ if any(fnmatch.fnmatch(str(collection_path), str(glob)) for glob in ignore_globs):
+ return True
+
+ allow_in_venv = config.getoption("collect_in_virtualenv")
+ if not allow_in_venv and _in_venv(collection_path):
+ return True
+
+ if collection_path.is_dir():
+ norecursepatterns = config.getini("norecursedirs")
+ if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
+ return True
+
+ return None
+
+
+def pytest_collect_directory(
+ path: Path, parent: nodes.Collector
+) -> nodes.Collector | None:
+ return Dir.from_parent(parent, path=path)
+
+
+def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None:
+ deselect_prefixes = tuple(config.getoption("deselect") or [])
+ if not deselect_prefixes:
+ return
+
+ remaining = []
+ deselected = []
+ for colitem in items:
+ if colitem.nodeid.startswith(deselect_prefixes):
+ deselected.append(colitem)
+ else:
+ remaining.append(colitem)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+class FSHookProxy:
+ def __init__(
+ self,
+ pm: PytestPluginManager,
+ remove_mods: AbstractSet[object],
+ ) -> None:
+ self.pm = pm
+ self.remove_mods = remove_mods
+
+ def __getattr__(self, name: str) -> pluggy.HookCaller:
+ x = self.pm.subset_hook_caller(name, remove_plugins=self.remove_mods)
+ self.__dict__[name] = x
+ return x
+
+
+class Interrupted(KeyboardInterrupt):
+ """Signals that the test run was interrupted."""
+
+ __module__ = "builtins" # For py3.
+
+
+class Failed(Exception):
+ """Signals a stop as failed test run."""
+
+
+@dataclasses.dataclass
+class _bestrelpath_cache(dict[Path, str]):
+ __slots__ = ("path",)
+
+ path: Path
+
+ def __missing__(self, path: Path) -> str:
+ r = bestrelpath(self.path, path)
+ self[path] = r
+ return r
+
+
+@final
+class Dir(nodes.Directory):
+ """Collector of files in a file system directory.
+
+ .. versionadded:: 8.0
+
+ .. note::
+
+ Python directories with an `__init__.py` file are instead collected by
+ :class:`~pytest.Package` by default. Both are :class:`~pytest.Directory`
+ collectors.
+ """
+
+ @classmethod
+ def from_parent( # type: ignore[override]
+ cls,
+ parent: nodes.Collector,
+ *,
+ path: Path,
+ ) -> Self:
+ """The public constructor.
+
+ :param parent: The parent collector of this Dir.
+ :param path: The directory's path.
+ :type path: pathlib.Path
+ """
+ return super().from_parent(parent=parent, path=path)
+
+ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
+ config = self.config
+ col: nodes.Collector | None
+ cols: Sequence[nodes.Collector]
+ ihook = self.ihook
+ for direntry in scandir(self.path):
+ if direntry.is_dir():
+ path = Path(direntry.path)
+ if not self.session.isinitpath(path, with_parents=True):
+ if ihook.pytest_ignore_collect(collection_path=path, config=config):
+ continue
+ col = ihook.pytest_collect_directory(path=path, parent=self)
+ if col is not None:
+ yield col
+
+ elif direntry.is_file():
+ path = Path(direntry.path)
+ if not self.session.isinitpath(path):
+ if ihook.pytest_ignore_collect(collection_path=path, config=config):
+ continue
+ cols = ihook.pytest_collect_file(file_path=path, parent=self)
+ yield from cols
+
+
+@final
+class Session(nodes.Collector):
+ """The root of the collection tree.
+
+ ``Session`` collects the initial paths given as arguments to pytest.
+ """
+
+ Interrupted = Interrupted
+ Failed = Failed
+ # Set on the session by runner.pytest_sessionstart.
+ _setupstate: SetupState
+ # Set on the session by fixtures.pytest_sessionstart.
+ _fixturemanager: FixtureManager
+ exitstatus: int | ExitCode
+
+ def __init__(self, config: Config) -> None:
+ super().__init__(
+ name="",
+ path=config.rootpath,
+ fspath=None,
+ parent=None,
+ config=config,
+ session=self,
+ nodeid="",
+ )
+ self.testsfailed = 0
+ self.testscollected = 0
+ self._shouldstop: bool | str = False
+ self._shouldfail: bool | str = False
+ self.trace = config.trace.root.get("collection")
+ self._initialpaths: frozenset[Path] = frozenset()
+ self._initialpaths_with_parents: frozenset[Path] = frozenset()
+ self._notfound: list[tuple[str, Sequence[nodes.Collector]]] = []
+ self._initial_parts: list[CollectionArgument] = []
+ self._collection_cache: dict[nodes.Collector, CollectReport] = {}
+ self.items: list[nodes.Item] = []
+
+ self._bestrelpathcache: dict[Path, str] = _bestrelpath_cache(config.rootpath)
+
+ self.config.pluginmanager.register(self, name="session")
+
+ @classmethod
+ def from_config(cls, config: Config) -> Session:
+ session: Session = cls._create(config=config)
+ return session
+
+ def __repr__(self) -> str:
+ return (
+ f"<{self.__class__.__name__} {self.name} "
+ f"exitstatus=%r "
+ f"testsfailed={self.testsfailed} "
+ f"testscollected={self.testscollected}>"
+ ) % getattr(self, "exitstatus", "")
+
+ @property
+ def shouldstop(self) -> bool | str:
+ return self._shouldstop
+
+ @shouldstop.setter
+ def shouldstop(self, value: bool | str) -> None:
+ # The runner checks shouldfail and assumes that if it is set we are
+ # definitely stopping, so prevent unsetting it.
+ if value is False and self._shouldstop:
+ warnings.warn(
+ PytestWarning(
+ "session.shouldstop cannot be unset after it has been set; ignoring."
+ ),
+ stacklevel=2,
+ )
+ return
+ self._shouldstop = value
+
+ @property
+ def shouldfail(self) -> bool | str:
+ return self._shouldfail
+
+ @shouldfail.setter
+ def shouldfail(self, value: bool | str) -> None:
+ # The runner checks shouldfail and assumes that if it is set we are
+ # definitely stopping, so prevent unsetting it.
+ if value is False and self._shouldfail:
+ warnings.warn(
+ PytestWarning(
+ "session.shouldfail cannot be unset after it has been set; ignoring."
+ ),
+ stacklevel=2,
+ )
+ return
+ self._shouldfail = value
+
+ @property
+ def startpath(self) -> Path:
+ """The path from which pytest was invoked.
+
+ .. versionadded:: 7.0.0
+ """
+ return self.config.invocation_params.dir
+
+ def _node_location_to_relpath(self, node_path: Path) -> str:
+ # bestrelpath is a quite slow function.
+ return self._bestrelpathcache[node_path]
+
+ @hookimpl(tryfirst=True)
+ def pytest_collectstart(self) -> None:
+ if self.shouldfail:
+ raise self.Failed(self.shouldfail)
+ if self.shouldstop:
+ raise self.Interrupted(self.shouldstop)
+
+ @hookimpl(tryfirst=True)
+ def pytest_runtest_logreport(self, report: TestReport | CollectReport) -> None:
+ if report.failed and not hasattr(report, "wasxfail"):
+ self.testsfailed += 1
+ maxfail = self.config.getvalue("maxfail")
+ if maxfail and self.testsfailed >= maxfail:
+ self.shouldfail = f"stopping after {self.testsfailed} failures"
+
+ pytest_collectreport = pytest_runtest_logreport
+
+ def isinitpath(
+ self,
+ path: str | os.PathLike[str],
+ *,
+ with_parents: bool = False,
+ ) -> bool:
+ """Is path an initial path?
+
+ An initial path is a path explicitly given to pytest on the command
+ line.
+
+ :param with_parents:
+ If set, also return True if the path is a parent of an initial path.
+
+ .. versionchanged:: 8.0
+ Added the ``with_parents`` parameter.
+ """
+ # Optimization: Path(Path(...)) is much slower than isinstance.
+ path_ = path if isinstance(path, Path) else Path(path)
+ if with_parents:
+ return path_ in self._initialpaths_with_parents
+ else:
+ return path_ in self._initialpaths
+
+ def gethookproxy(self, fspath: os.PathLike[str]) -> pluggy.HookRelay:
+ # Optimization: Path(Path(...)) is much slower than isinstance.
+ path = fspath if isinstance(fspath, Path) else Path(fspath)
+ pm = self.config.pluginmanager
+ # Check if we have the common case of running
+ # hooks with all conftest.py files.
+ my_conftestmodules = pm._getconftestmodules(path)
+ remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
+ proxy: pluggy.HookRelay
+ if remove_mods:
+ # One or more conftests are not in use at this path.
+ proxy = FSHookProxy(pm, remove_mods) # type: ignore[assignment]
+ else:
+ # All plugins are active for this fspath.
+ proxy = self.config.hook
+ return proxy
+
+ def _collect_path(
+ self,
+ path: Path,
+ path_cache: dict[Path, Sequence[nodes.Collector]],
+ ) -> Sequence[nodes.Collector]:
+ """Create a Collector for the given path.
+
+ `path_cache` makes it so the same Collectors are returned for the same
+ path.
+ """
+ if path in path_cache:
+ return path_cache[path]
+
+ if path.is_dir():
+ ihook = self.gethookproxy(path.parent)
+ col: nodes.Collector | None = ihook.pytest_collect_directory(
+ path=path, parent=self
+ )
+ cols: Sequence[nodes.Collector] = (col,) if col is not None else ()
+
+ elif path.is_file():
+ ihook = self.gethookproxy(path)
+ cols = ihook.pytest_collect_file(file_path=path, parent=self)
+
+ else:
+ # Broken symlink or invalid/missing file.
+ cols = ()
+
+ path_cache[path] = cols
+ return cols
+
+ @overload
+ def perform_collect(
+ self, args: Sequence[str] | None = ..., genitems: Literal[True] = ...
+ ) -> Sequence[nodes.Item]: ...
+
+ @overload
+ def perform_collect(
+ self, args: Sequence[str] | None = ..., genitems: bool = ...
+ ) -> Sequence[nodes.Item | nodes.Collector]: ...
+
+ def perform_collect(
+ self, args: Sequence[str] | None = None, genitems: bool = True
+ ) -> Sequence[nodes.Item | nodes.Collector]:
+ """Perform the collection phase for this session.
+
+ This is called by the default :hook:`pytest_collection` hook
+ implementation; see the documentation of this hook for more details.
+ For testing purposes, it may also be called directly on a fresh
+ ``Session``.
+
+ This function normally recursively expands any collectors collected
+ from the session to their items, and only items are returned. For
+ testing purposes, this may be suppressed by passing ``genitems=False``,
+ in which case the return value contains these collectors unexpanded,
+ and ``session.items`` is empty.
+ """
+ if args is None:
+ args = self.config.args
+
+ self.trace("perform_collect", self, args)
+ self.trace.root.indent += 1
+
+ hook = self.config.hook
+
+ self._notfound = []
+ self._initial_parts = []
+ self._collection_cache = {}
+ self.items = []
+ items: Sequence[nodes.Item | nodes.Collector] = self.items
+ consider_namespace_packages: bool = self.config.getini(
+ "consider_namespace_packages"
+ )
+ try:
+ initialpaths: list[Path] = []
+ initialpaths_with_parents: list[Path] = []
+
+ collection_args = [
+ resolve_collection_argument(
+ self.config.invocation_params.dir,
+ arg,
+ i,
+ as_pypath=self.config.option.pyargs,
+ consider_namespace_packages=consider_namespace_packages,
+ )
+ for i, arg in enumerate(args)
+ ]
+
+ if not self.config.getoption("keepduplicates"):
+ # Normalize the collection arguments -- remove duplicates and overlaps.
+ self._initial_parts = normalize_collection_arguments(collection_args)
+ else:
+ self._initial_parts = collection_args
+
+ for collection_argument in self._initial_parts:
+ initialpaths.append(collection_argument.path)
+ initialpaths_with_parents.append(collection_argument.path)
+ initialpaths_with_parents.extend(collection_argument.path.parents)
+ self._initialpaths = frozenset(initialpaths)
+ self._initialpaths_with_parents = frozenset(initialpaths_with_parents)
+
+ rep = collect_one_node(self)
+ self.ihook.pytest_collectreport(report=rep)
+ self.trace.root.indent -= 1
+ if self._notfound:
+ errors = []
+ for arg, collectors in self._notfound:
+ if collectors:
+ errors.append(
+ f"not found: {arg}\n(no match in any of {collectors!r})"
+ )
+ else:
+ errors.append(f"found no collectors for {arg}")
+
+ raise UsageError(*errors)
+
+ if not genitems:
+ items = rep.result
+ else:
+ if rep.passed:
+ for node in rep.result:
+ self.items.extend(self.genitems(node))
+
+ self.config.pluginmanager.check_pending()
+ hook.pytest_collection_modifyitems(
+ session=self, config=self.config, items=items
+ )
+ finally:
+ self._notfound = []
+ self._initial_parts = []
+ self._collection_cache = {}
+ hook.pytest_collection_finish(session=self)
+
+ if genitems:
+ self.testscollected = len(items)
+
+ return items
+
+ def _collect_one_node(
+ self,
+ node: nodes.Collector,
+ handle_dupes: bool = True,
+ ) -> tuple[CollectReport, bool]:
+ if node in self._collection_cache and handle_dupes:
+ rep = self._collection_cache[node]
+ return rep, True
+ else:
+ rep = collect_one_node(node)
+ self._collection_cache[node] = rep
+ return rep, False
+
+ def collect(self) -> Iterator[nodes.Item | nodes.Collector]:
+ # This is a cache for the root directories of the initial paths.
+ # We can't use collection_cache for Session because of its special
+ # role as the bootstrapping collector.
+ path_cache: dict[Path, Sequence[nodes.Collector]] = {}
+
+ pm = self.config.pluginmanager
+
+ for collection_argument in self._initial_parts:
+ self.trace("processing argument", collection_argument)
+ self.trace.root.indent += 1
+
+ argpath = collection_argument.path
+ names = collection_argument.parts
+ parametrization = collection_argument.parametrization
+ module_name = collection_argument.module_name
+
+ # resolve_collection_argument() ensures this.
+ if argpath.is_dir():
+ assert not names, f"invalid arg {(argpath, names)!r}"
+
+ paths = [argpath]
+ # Add relevant parents of the path, from the root, e.g.
+ # /a/b/c.py -> [/, /a, /a/b, /a/b/c.py]
+ if module_name is None:
+ # Paths outside of the confcutdir should not be considered.
+ for path in argpath.parents:
+ if not pm._is_in_confcutdir(path):
+ break
+ paths.insert(0, path)
+ else:
+ # For --pyargs arguments, only consider paths matching the module
+ # name. Paths beyond the package hierarchy are not included.
+ module_name_parts = module_name.split(".")
+ for i, path in enumerate(argpath.parents, 2):
+ if i > len(module_name_parts) or path.stem != module_name_parts[-i]:
+ break
+ paths.insert(0, path)
+
+ # Start going over the parts from the root, collecting each level
+ # and discarding all nodes which don't match the level's part.
+ any_matched_in_initial_part = False
+ notfound_collectors = []
+ work: list[tuple[nodes.Collector | nodes.Item, list[Path | str]]] = [
+ (self, [*paths, *names])
+ ]
+ while work:
+ matchnode, matchparts = work.pop()
+
+ # Pop'd all of the parts, this is a match.
+ if not matchparts:
+ yield matchnode
+ any_matched_in_initial_part = True
+ continue
+
+ # Should have been matched by now, discard.
+ if not isinstance(matchnode, nodes.Collector):
+ continue
+
+ # Collect this level of matching.
+ # Collecting Session (self) is done directly to avoid endless
+ # recursion to this function.
+ subnodes: Sequence[nodes.Collector | nodes.Item]
+ if isinstance(matchnode, Session):
+ assert isinstance(matchparts[0], Path)
+ subnodes = matchnode._collect_path(matchparts[0], path_cache)
+ else:
+ # For backward compat, files given directly multiple
+ # times on the command line should not be deduplicated.
+ handle_dupes = not (
+ len(matchparts) == 1
+ and isinstance(matchparts[0], Path)
+ and matchparts[0].is_file()
+ )
+ rep, duplicate = self._collect_one_node(matchnode, handle_dupes)
+ if not duplicate and not rep.passed:
+ # Report collection failures here to avoid failing to
+ # run some test specified in the command line because
+ # the module could not be imported (#134).
+ matchnode.ihook.pytest_collectreport(report=rep)
+ if not rep.passed:
+ continue
+ subnodes = rep.result
+
+ # Prune this level.
+ any_matched_in_collector = False
+ for node in reversed(subnodes):
+ # Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
+ if isinstance(matchparts[0], Path):
+ is_match = node.path == matchparts[0]
+ if sys.platform == "win32" and not is_match:
+ # In case the file paths do not match, fallback to samefile() to
+ # account for short-paths on Windows (#11895). But use a version
+ # which doesn't resolve symlinks, otherwise we might match the
+ # same file more than once (#12039).
+ is_match = samefile_nofollow(node.path, matchparts[0])
+
+ # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
+ else:
+ if len(matchparts) == 1:
+ # This the last part, one parametrization goes.
+ if parametrization is not None:
+ # A parametrized arg must match exactly.
+ is_match = node.name == matchparts[0] + parametrization
+ else:
+ # A non-parameterized arg matches all parametrizations (if any).
+ # TODO: Remove the hacky split once the collection structure
+ # contains parametrization.
+ is_match = node.name.split("[")[0] == matchparts[0]
+ else:
+ is_match = node.name == matchparts[0]
+ if is_match:
+ work.append((node, matchparts[1:]))
+ any_matched_in_collector = True
+
+ if not any_matched_in_collector:
+ notfound_collectors.append(matchnode)
+
+ if not any_matched_in_initial_part:
+ report_arg = "::".join((str(argpath), *names))
+ self._notfound.append((report_arg, notfound_collectors))
+
+ self.trace.root.indent -= 1
+
+ def genitems(self, node: nodes.Item | nodes.Collector) -> Iterator[nodes.Item]:
+ self.trace("genitems", node)
+ if isinstance(node, nodes.Item):
+ node.ihook.pytest_itemcollected(item=node)
+ yield node
+ else:
+ assert isinstance(node, nodes.Collector)
+ # For backward compat, dedup only applies to files.
+ handle_dupes = not isinstance(node, nodes.File)
+ rep, duplicate = self._collect_one_node(node, handle_dupes)
+ if rep.passed:
+ for subnode in rep.result:
+ yield from self.genitems(subnode)
+ if not duplicate:
+ node.ihook.pytest_collectreport(report=rep)
+
+
+def search_pypath(
+ module_name: str, *, consider_namespace_packages: bool = False
+) -> str | None:
+ """Search sys.path for the given a dotted module name, and return its file
+ system path if found."""
+ try:
+ spec = importlib.util.find_spec(module_name)
+ # AttributeError: looks like package module, but actually filename
+ # ImportError: module does not exist
+ # ValueError: not a module name
+ except (AttributeError, ImportError, ValueError):
+ return None
+
+ if spec is None:
+ return None
+
+ if (
+ spec.submodule_search_locations is None
+ or len(spec.submodule_search_locations) == 0
+ ):
+ # Must be a simple module.
+ return spec.origin
+
+ if consider_namespace_packages:
+ # If submodule_search_locations is set, it's a package (regular or namespace).
+ # Typically there is a single entry, but documentation claims it can be empty too
+ # (e.g. if the package has no physical location).
+ return spec.submodule_search_locations[0]
+
+ if spec.origin is None:
+ # This is only the case for namespace packages
+ return None
+
+ return os.path.dirname(spec.origin)
+
+
+@dataclasses.dataclass(frozen=True)
+class CollectionArgument:
+ """A resolved collection argument."""
+
+ path: Path
+ parts: Sequence[str]
+ parametrization: str | None
+ module_name: str | None
+ original_index: int
+
+
+def resolve_collection_argument(
+ invocation_path: Path,
+ arg: str,
+ arg_index: int,
+ *,
+ as_pypath: bool = False,
+ consider_namespace_packages: bool = False,
+) -> CollectionArgument:
+ """Parse path arguments optionally containing selection parts and return (fspath, names).
+
+ Command-line arguments can point to files and/or directories, and optionally contain
+ parts for specific tests selection, for example:
+
+ "pkg/tests/test_foo.py::TestClass::test_foo"
+
+ This function ensures the path exists, and returns a resolved `CollectionArgument`:
+
+ CollectionArgument(
+ path=Path("/full/path/to/pkg/tests/test_foo.py"),
+ parts=["TestClass", "test_foo"],
+ module_name=None,
+ )
+
+ When as_pypath is True, expects that the command-line argument actually contains
+ module paths instead of file-system paths:
+
+ "pkg.tests.test_foo::TestClass::test_foo[a,b]"
+
+ In which case we search sys.path for a matching module, and then return the *path* to the
+ found module, which may look like this:
+
+ CollectionArgument(
+ path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"),
+ parts=["TestClass", "test_foo"],
+ parametrization="[a,b]",
+ module_name="pkg.tests.test_foo",
+ )
+
+ If the path doesn't exist, raise UsageError.
+ If the path is a directory and selection parts are present, raise UsageError.
+ """
+ base, squacket, rest = arg.partition("[")
+ strpath, *parts = base.split("::")
+ if squacket and not parts:
+ raise UsageError(f"path cannot contain [] parametrization: {arg}")
+ parametrization = f"{squacket}{rest}" if squacket else None
+ module_name = None
+ if as_pypath:
+ pyarg_strpath = search_pypath(
+ strpath, consider_namespace_packages=consider_namespace_packages
+ )
+ if pyarg_strpath is not None:
+ module_name = strpath
+ strpath = pyarg_strpath
+ fspath = invocation_path / strpath
+ fspath = absolutepath(fspath)
+ if not safe_exists(fspath):
+ msg = (
+ "module or package not found: {arg} (missing __init__.py?)"
+ if as_pypath
+ else "file or directory not found: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ if parts and fspath.is_dir():
+ msg = (
+ "package argument cannot contain :: selection parts: {arg}"
+ if as_pypath
+ else "directory argument cannot contain :: selection parts: {arg}"
+ )
+ raise UsageError(msg.format(arg=arg))
+ return CollectionArgument(
+ path=fspath,
+ parts=parts,
+ parametrization=parametrization,
+ module_name=module_name,
+ original_index=arg_index,
+ )
+
+
+def is_collection_argument_subsumed_by(
+ arg: CollectionArgument, by: CollectionArgument
+) -> bool:
+ """Check if `arg` is subsumed (contained) by `by`."""
+ # First check path subsumption.
+ if by.path != arg.path:
+ # `by` subsumes `arg` if `by` is a parent directory of `arg` and has no
+ # parts (collects everything in that directory).
+ if not by.parts:
+ return arg.path.is_relative_to(by.path)
+ return False
+ # Paths are equal, check parts.
+ # For example: ("TestClass",) is a prefix of ("TestClass", "test_method").
+ if len(by.parts) > len(arg.parts) or arg.parts[: len(by.parts)] != by.parts:
+ return False
+ # Paths and parts are equal, check parametrization.
+ # A `by` without parametrization (None) matches everything, e.g.
+ # `pytest x.py::test_it` matches `x.py::test_it[0]`. Otherwise must be
+ # exactly equal.
+ if by.parametrization is not None and by.parametrization != arg.parametrization:
+ return False
+ return True
+
+
+def normalize_collection_arguments(
+ collection_args: Sequence[CollectionArgument],
+) -> list[CollectionArgument]:
+ """Normalize collection arguments to eliminate overlapping paths and parts.
+
+ Detects when collection arguments overlap in either paths or parts and only
+ keeps the shorter prefix, or the earliest argument if duplicate, preserving
+ order. The result is prefix-free.
+ """
+ # A quadratic algorithm is not acceptable since large inputs are possible.
+ # So this uses an O(n*log(n)) algorithm which takes advantage of the
+ # property that after sorting, a collection argument will immediately
+ # precede collection arguments it subsumes. An O(n) algorithm is not worth
+ # it.
+ collection_args_sorted = sorted(
+ collection_args,
+ key=lambda arg: (arg.path, arg.parts, arg.parametrization or ""),
+ )
+ normalized: list[CollectionArgument] = []
+ last_kept = None
+ for arg in collection_args_sorted:
+ if last_kept is None or not is_collection_argument_subsumed_by(arg, last_kept):
+ normalized.append(arg)
+ last_kept = arg
+ normalized.sort(key=lambda arg: arg.original_index)
+ return normalized
diff --git a/venv/lib/python3.11/site-packages/_pytest/mark/__init__.py b/venv/lib/python3.11/site-packages/_pytest/mark/__init__.py
new file mode 100644
index 000000000..56c407ab3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/mark/__init__.py
@@ -0,0 +1,301 @@
+"""Generic mechanism for marking and selecting python functions."""
+
+from __future__ import annotations
+
+import collections
+from collections.abc import Collection
+from collections.abc import Iterable
+from collections.abc import Set as AbstractSet
+import dataclasses
+from typing import TYPE_CHECKING
+
+from .expression import Expression
+from .structures import _HiddenParam
+from .structures import EMPTY_PARAMETERSET_OPTION
+from .structures import get_empty_parameterset_mark
+from .structures import HIDDEN_PARAM
+from .structures import Mark
+from .structures import MARK_GEN
+from .structures import MarkDecorator
+from .structures import MarkGenerator
+from .structures import ParameterSet
+from _pytest.compat import NOTSET
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import UsageError
+from _pytest.config.argparsing import Parser
+from _pytest.stash import StashKey
+
+
+if TYPE_CHECKING:
+ from _pytest.nodes import Item
+
+
+__all__ = [
+ "HIDDEN_PARAM",
+ "MARK_GEN",
+ "Mark",
+ "MarkDecorator",
+ "MarkGenerator",
+ "ParameterSet",
+ "get_empty_parameterset_mark",
+]
+
+
+old_mark_config_key = StashKey[Config | None]()
+
+
+def param(
+ *values: object,
+ marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
+ id: str | _HiddenParam | None = None,
+) -> ParameterSet:
+ """Specify a parameter in `pytest.mark.parametrize`_ calls or
+ :ref:`parametrized fixtures `.
+
+ .. code-block:: python
+
+ @pytest.mark.parametrize(
+ "test_input,expected",
+ [
+ ("3+5", 8),
+ pytest.param("6*9", 42, marks=pytest.mark.xfail),
+ ],
+ )
+ def test_eval(test_input, expected):
+ assert eval(test_input) == expected
+
+ :param values: Variable args of the values of the parameter set, in order.
+
+ :param marks:
+ A single mark or a list of marks to be applied to this parameter set.
+
+ :ref:`pytest.mark.usefixtures ` cannot be added via this parameter.
+
+ :type id: str | Literal[pytest.HIDDEN_PARAM] | None
+ :param id:
+ The id to attribute to this parameter set.
+
+ .. versionadded:: 8.4
+ :ref:`hidden-param` means to hide the parameter set
+ from the test name. Can only be used at most 1 time, as
+ test names need to be unique.
+ """
+ return ParameterSet.param(*values, marks=marks, id=id)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group._addoption( # private to use reserved lower-case short option
+ "-k",
+ action="store",
+ dest="keyword",
+ default="",
+ metavar="EXPRESSION",
+ help="Only run tests which match the given substring expression. "
+ "An expression is a Python evaluable expression "
+ "where all names are substring-matched against test names "
+ "and their parent classes. Example: -k 'test_method or test_"
+ "other' matches all test functions and classes whose name "
+ "contains 'test_method' or 'test_other', while -k 'not test_method' "
+ "matches those that don't contain 'test_method' in their names. "
+ "-k 'not test_method and not test_other' will eliminate the matches. "
+ "Additionally keywords are matched to classes and functions "
+ "containing extra names in their 'extra_keyword_matches' set, "
+ "as well as functions which have names assigned directly to them. "
+ "The matching is case-insensitive.",
+ )
+
+ group._addoption( # private to use reserved lower-case short option
+ "-m",
+ action="store",
+ dest="markexpr",
+ default="",
+ metavar="MARKEXPR",
+ help="Only run tests matching given mark expression. "
+ "For example: -m 'mark1 and not mark2'.",
+ )
+
+ group.addoption(
+ "--markers",
+ action="store_true",
+ help="show markers (builtin, plugin and per-project ones).",
+ )
+
+ parser.addini("markers", "Register new markers for test functions", "linelist")
+ parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
+
+
+@hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
+ import _pytest.config
+
+ if config.option.markers:
+ config._do_configure()
+ tw = _pytest.config.create_terminal_writer(config)
+ for line in config.getini("markers"):
+ parts = line.split(":", 1)
+ name = parts[0]
+ rest = parts[1] if len(parts) == 2 else ""
+ tw.write(f"@pytest.mark.{name}:", bold=True)
+ tw.line(rest)
+ tw.line()
+ config._ensure_unconfigure()
+ return 0
+
+ return None
+
+
+@dataclasses.dataclass
+class KeywordMatcher:
+ """A matcher for keywords.
+
+ Given a list of names, matches any substring of one of these names. The
+ string inclusion check is case-insensitive.
+
+ Will match on the name of colitem, including the names of its parents.
+ Only matches names of items which are either a :class:`Class` or a
+ :class:`Function`.
+
+ Additionally, matches on names in the 'extra_keyword_matches' set of
+ any item, as well as names directly assigned to test functions.
+ """
+
+ __slots__ = ("_names",)
+
+ _names: AbstractSet[str]
+
+ @classmethod
+ def from_item(cls, item: Item) -> KeywordMatcher:
+ mapped_names = set()
+
+ # Add the names of the current item and any parent items,
+ # except the Session and root Directory's which are not
+ # interesting for matching.
+ import pytest
+
+ for node in item.listchain():
+ if isinstance(node, pytest.Session):
+ continue
+ if isinstance(node, pytest.Directory) and isinstance(
+ node.parent, pytest.Session
+ ):
+ continue
+ mapped_names.add(node.name)
+
+ # Add the names added as extra keywords to current or parent items.
+ mapped_names.update(item.listextrakeywords())
+
+ # Add the names attached to the current function through direct assignment.
+ function_obj = getattr(item, "function", None)
+ if function_obj:
+ mapped_names.update(function_obj.__dict__)
+
+ # Add the markers to the keywords as we no longer handle them correctly.
+ mapped_names.update(mark.name for mark in item.iter_markers())
+
+ return cls(mapped_names)
+
+ def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool:
+ if kwargs:
+ raise UsageError("Keyword expressions do not support call parameters.")
+ subname = subname.lower()
+ return any(subname in name.lower() for name in self._names)
+
+
+def deselect_by_keyword(items: list[Item], config: Config) -> None:
+ keywordexpr = config.option.keyword.lstrip()
+ if not keywordexpr:
+ return
+
+ expr = _parse_expression(keywordexpr, "Wrong expression passed to '-k'")
+
+ remaining = []
+ deselected = []
+ for colitem in items:
+ if not expr.evaluate(KeywordMatcher.from_item(colitem)):
+ deselected.append(colitem)
+ else:
+ remaining.append(colitem)
+
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+@dataclasses.dataclass
+class MarkMatcher:
+ """A matcher for markers which are present.
+
+ Tries to match on any marker names, attached to the given colitem.
+ """
+
+ __slots__ = ("own_mark_name_mapping",)
+
+ own_mark_name_mapping: dict[str, list[Mark]]
+
+ @classmethod
+ def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher:
+ mark_name_mapping = collections.defaultdict(list)
+ for mark in markers:
+ mark_name_mapping[mark.name].append(mark)
+ return cls(mark_name_mapping)
+
+ def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool:
+ if not (matches := self.own_mark_name_mapping.get(name, [])):
+ return False
+
+ for mark in matches: # pylint: disable=consider-using-any-or-all
+ if all(mark.kwargs.get(k, NOTSET) == v for k, v in kwargs.items()):
+ return True
+ return False
+
+
+def deselect_by_mark(items: list[Item], config: Config) -> None:
+ matchexpr = config.option.markexpr
+ if not matchexpr:
+ return
+
+ expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'")
+ remaining: list[Item] = []
+ deselected: list[Item] = []
+ for item in items:
+ if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())):
+ remaining.append(item)
+ else:
+ deselected.append(item)
+ if deselected:
+ config.hook.pytest_deselected(items=deselected)
+ items[:] = remaining
+
+
+def _parse_expression(expr: str, exc_message: str) -> Expression:
+ try:
+ return Expression.compile(expr)
+ except SyntaxError as e:
+ raise UsageError(
+ f"{exc_message}: {e.text}: at column {e.offset}: {e.msg}"
+ ) from None
+
+
+def pytest_collection_modifyitems(items: list[Item], config: Config) -> None:
+ deselect_by_keyword(items, config)
+ deselect_by_mark(items, config)
+
+
+def pytest_configure(config: Config) -> None:
+ config.stash[old_mark_config_key] = MARK_GEN._config
+ MARK_GEN._config = config
+
+ empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
+
+ if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
+ raise UsageError(
+ f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect"
+ f" but it is {empty_parameterset!r}"
+ )
+
+
+def pytest_unconfigure(config: Config) -> None:
+ MARK_GEN._config = config.stash.get(old_mark_config_key, None)
diff --git a/venv/lib/python3.11/site-packages/_pytest/mark/expression.py b/venv/lib/python3.11/site-packages/_pytest/mark/expression.py
new file mode 100644
index 000000000..4b4a68d8a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/mark/expression.py
@@ -0,0 +1,353 @@
+r"""Evaluate match expressions, as used by `-k` and `-m`.
+
+The grammar is:
+
+expression: expr? EOF
+expr: and_expr ('or' and_expr)*
+and_expr: not_expr ('and' not_expr)*
+not_expr: 'not' not_expr | '(' expr ')' | ident kwargs?
+
+ident: (\w|:|\+|-|\.|\[|\]|\\|/)+
+kwargs: ('(' name '=' value ( ', ' name '=' value )* ')')
+name: a valid ident, but not a reserved keyword
+value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None'
+
+The semantics are:
+
+- Empty expression evaluates to False.
+- ident evaluates to True or False according to a provided matcher function.
+- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function.
+- or/and/not evaluate according to the usual boolean semantics.
+"""
+
+from __future__ import annotations
+
+import ast
+from collections.abc import Iterator
+from collections.abc import Mapping
+from collections.abc import Sequence
+import dataclasses
+import enum
+import keyword
+import re
+import types
+from typing import Final
+from typing import final
+from typing import Literal
+from typing import NoReturn
+from typing import overload
+from typing import Protocol
+
+
+__all__ = [
+ "Expression",
+ "ExpressionMatcher",
+]
+
+
+FILE_NAME: Final = ""
+
+
+class TokenType(enum.Enum):
+ LPAREN = "left parenthesis"
+ RPAREN = "right parenthesis"
+ OR = "or"
+ AND = "and"
+ NOT = "not"
+ IDENT = "identifier"
+ EOF = "end of input"
+ EQUAL = "="
+ STRING = "string literal"
+ COMMA = ","
+
+
+@dataclasses.dataclass(frozen=True)
+class Token:
+ __slots__ = ("pos", "type", "value")
+ type: TokenType
+ value: str
+ pos: int
+
+
+class Scanner:
+ __slots__ = ("current", "input", "tokens")
+
+ def __init__(self, input: str) -> None:
+ self.input = input
+ self.tokens = self.lex(input)
+ self.current = next(self.tokens)
+
+ def lex(self, input: str) -> Iterator[Token]:
+ pos = 0
+ while pos < len(input):
+ if input[pos] in (" ", "\t"):
+ pos += 1
+ elif input[pos] == "(":
+ yield Token(TokenType.LPAREN, "(", pos)
+ pos += 1
+ elif input[pos] == ")":
+ yield Token(TokenType.RPAREN, ")", pos)
+ pos += 1
+ elif input[pos] == "=":
+ yield Token(TokenType.EQUAL, "=", pos)
+ pos += 1
+ elif input[pos] == ",":
+ yield Token(TokenType.COMMA, ",", pos)
+ pos += 1
+ elif (quote_char := input[pos]) in ("'", '"'):
+ end_quote_pos = input.find(quote_char, pos + 1)
+ if end_quote_pos == -1:
+ raise SyntaxError(
+ f'closing quote "{quote_char}" is missing',
+ (FILE_NAME, 1, pos + 1, input),
+ )
+ value = input[pos : end_quote_pos + 1]
+ if (backslash_pos := value.find("\\")) != -1:
+ raise SyntaxError(
+ r'escaping with "\" not supported in marker expression',
+ (FILE_NAME, 1, pos + backslash_pos + 1, input),
+ )
+ yield Token(TokenType.STRING, value, pos)
+ pos += len(value)
+ else:
+ match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:])
+ if match:
+ value = match.group(0)
+ if value == "or":
+ yield Token(TokenType.OR, value, pos)
+ elif value == "and":
+ yield Token(TokenType.AND, value, pos)
+ elif value == "not":
+ yield Token(TokenType.NOT, value, pos)
+ else:
+ yield Token(TokenType.IDENT, value, pos)
+ pos += len(value)
+ else:
+ raise SyntaxError(
+ f'unexpected character "{input[pos]}"',
+ (FILE_NAME, 1, pos + 1, input),
+ )
+ yield Token(TokenType.EOF, "", pos)
+
+ @overload
+ def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ...
+
+ @overload
+ def accept(
+ self, type: TokenType, *, reject: Literal[False] = False
+ ) -> Token | None: ...
+
+ def accept(self, type: TokenType, *, reject: bool = False) -> Token | None:
+ if self.current.type is type:
+ token = self.current
+ if token.type is not TokenType.EOF:
+ self.current = next(self.tokens)
+ return token
+ if reject:
+ self.reject((type,))
+ return None
+
+ def reject(self, expected: Sequence[TokenType]) -> NoReturn:
+ raise SyntaxError(
+ "expected {}; got {}".format(
+ " OR ".join(type.value for type in expected),
+ self.current.type.value,
+ ),
+ (FILE_NAME, 1, self.current.pos + 1, self.input),
+ )
+
+
+# True, False and None are legal match expression identifiers,
+# but illegal as Python identifiers. To fix this, this prefix
+# is added to identifiers in the conversion to Python AST.
+IDENT_PREFIX = "$"
+
+
+def expression(s: Scanner) -> ast.Expression:
+ if s.accept(TokenType.EOF):
+ ret: ast.expr = ast.Constant(False)
+ else:
+ ret = expr(s)
+ s.accept(TokenType.EOF, reject=True)
+ return ast.fix_missing_locations(ast.Expression(ret))
+
+
+def expr(s: Scanner) -> ast.expr:
+ ret = and_expr(s)
+ while s.accept(TokenType.OR):
+ rhs = and_expr(s)
+ ret = ast.BoolOp(ast.Or(), [ret, rhs])
+ return ret
+
+
+def and_expr(s: Scanner) -> ast.expr:
+ ret = not_expr(s)
+ while s.accept(TokenType.AND):
+ rhs = not_expr(s)
+ ret = ast.BoolOp(ast.And(), [ret, rhs])
+ return ret
+
+
+def not_expr(s: Scanner) -> ast.expr:
+ if s.accept(TokenType.NOT):
+ return ast.UnaryOp(ast.Not(), not_expr(s))
+ if s.accept(TokenType.LPAREN):
+ ret = expr(s)
+ s.accept(TokenType.RPAREN, reject=True)
+ return ret
+ ident = s.accept(TokenType.IDENT)
+ if ident:
+ name = ast.Name(IDENT_PREFIX + ident.value, ast.Load())
+ if s.accept(TokenType.LPAREN):
+ ret = ast.Call(func=name, args=[], keywords=all_kwargs(s))
+ s.accept(TokenType.RPAREN, reject=True)
+ else:
+ ret = name
+ return ret
+
+ s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT))
+
+
+BUILTIN_MATCHERS = {"True": True, "False": False, "None": None}
+
+
+def single_kwarg(s: Scanner) -> ast.keyword:
+ keyword_name = s.accept(TokenType.IDENT, reject=True)
+ if not keyword_name.value.isidentifier():
+ raise SyntaxError(
+ f"not a valid python identifier {keyword_name.value}",
+ (FILE_NAME, 1, keyword_name.pos + 1, s.input),
+ )
+ if keyword.iskeyword(keyword_name.value):
+ raise SyntaxError(
+ f"unexpected reserved python keyword `{keyword_name.value}`",
+ (FILE_NAME, 1, keyword_name.pos + 1, s.input),
+ )
+ s.accept(TokenType.EQUAL, reject=True)
+
+ if value_token := s.accept(TokenType.STRING):
+ value: str | int | bool | None = value_token.value[1:-1] # strip quotes
+ else:
+ value_token = s.accept(TokenType.IDENT, reject=True)
+ if (number := value_token.value).isdigit() or (
+ number.startswith("-") and number[1:].isdigit()
+ ):
+ value = int(number)
+ elif value_token.value in BUILTIN_MATCHERS:
+ value = BUILTIN_MATCHERS[value_token.value]
+ else:
+ raise SyntaxError(
+ f'unexpected character/s "{value_token.value}"',
+ (FILE_NAME, 1, value_token.pos + 1, s.input),
+ )
+
+ ret = ast.keyword(keyword_name.value, ast.Constant(value))
+ return ret
+
+
+def all_kwargs(s: Scanner) -> list[ast.keyword]:
+ ret = [single_kwarg(s)]
+ while s.accept(TokenType.COMMA):
+ ret.append(single_kwarg(s))
+ return ret
+
+
+class ExpressionMatcher(Protocol):
+ """A callable which, given an identifier and optional kwargs, should return
+ whether it matches in an :class:`Expression` evaluation.
+
+ Should be prepared to handle arbitrary strings as input.
+
+ If no kwargs are provided, the expression of the form `foo`.
+ If kwargs are provided, the expression is of the form `foo(1, b=True, "s")`.
+
+ If the expression is not supported (e.g. don't want to accept the kwargs
+ syntax variant), should raise :class:`~pytest.UsageError`.
+
+ Example::
+
+ def matcher(name: str, /, **kwargs: str | int | bool | None) -> bool:
+ # Match `cat`.
+ if name == "cat" and not kwargs:
+ return True
+ # Match `dog(barks=True)`.
+ if name == "dog" and kwargs == {"barks": False}:
+ return True
+ return False
+ """
+
+ def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ...
+
+
+@dataclasses.dataclass
+class MatcherNameAdapter:
+ matcher: ExpressionMatcher
+ name: str
+
+ def __bool__(self) -> bool:
+ return self.matcher(self.name)
+
+ def __call__(self, **kwargs: str | int | bool | None) -> bool:
+ return self.matcher(self.name, **kwargs)
+
+
+class MatcherAdapter(Mapping[str, MatcherNameAdapter]):
+ """Adapts a matcher function to a locals mapping as required by eval()."""
+
+ def __init__(self, matcher: ExpressionMatcher) -> None:
+ self.matcher = matcher
+
+ def __getitem__(self, key: str) -> MatcherNameAdapter:
+ return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :])
+
+ def __iter__(self) -> Iterator[str]:
+ raise NotImplementedError()
+
+ def __len__(self) -> int:
+ raise NotImplementedError()
+
+
+@final
+class Expression:
+ """A compiled match expression as used by -k and -m.
+
+ The expression can be evaluated against different matchers.
+ """
+
+ __slots__ = ("_code", "input")
+
+ def __init__(self, input: str, code: types.CodeType) -> None:
+ #: The original input line, as a string.
+ self.input: Final = input
+ self._code: Final = code
+
+ @classmethod
+ def compile(cls, input: str) -> Expression:
+ """Compile a match expression.
+
+ :param input: The input expression - one line.
+
+ :raises SyntaxError: If the expression is malformed.
+ """
+ astexpr = expression(Scanner(input))
+ code = compile(
+ astexpr,
+ filename="",
+ mode="eval",
+ )
+ return Expression(input, code)
+
+ def evaluate(self, matcher: ExpressionMatcher) -> bool:
+ """Evaluate the match expression.
+
+ :param matcher:
+ A callback which determines whether an identifier matches or not.
+ See the :class:`ExpressionMatcher` protocol for details and example.
+
+ :returns: Whether the expression matches or not.
+
+ :raises UsageError:
+ If the matcher doesn't support the expression. Cannot happen if the
+ matcher supports all expressions.
+ """
+ return bool(eval(self._code, {"__builtins__": {}}, MatcherAdapter(matcher)))
diff --git a/venv/lib/python3.11/site-packages/_pytest/mark/structures.py b/venv/lib/python3.11/site-packages/_pytest/mark/structures.py
new file mode 100644
index 000000000..5449b17a1
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/mark/structures.py
@@ -0,0 +1,695 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+import collections.abc
+from collections.abc import Callable
+from collections.abc import Collection
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Mapping
+from collections.abc import MutableMapping
+from collections.abc import Sequence
+import dataclasses
+import enum
+import inspect
+from typing import Any
+from typing import final
+from typing import NamedTuple
+from typing import overload
+from typing import TYPE_CHECKING
+from typing import TypeVar
+import warnings
+
+from .._code import getfslineno
+from ..compat import NOTSET
+from ..compat import NotSetType
+from _pytest.config import Config
+from _pytest.deprecated import check_ispytest
+from _pytest.deprecated import PARAMETRIZE_NON_COLLECTION_ITERABLE
+from _pytest.outcomes import fail
+from _pytest.raises import AbstractRaises
+from _pytest.scope import ScopeName
+from _pytest.warning_types import PytestCollectionWarning
+from _pytest.warning_types import PytestUnknownMarkWarning
+
+
+if TYPE_CHECKING:
+ from ..nodes import Node
+
+
+EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
+
+
+# Singleton type for HIDDEN_PARAM, as described in:
+# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
+class _HiddenParam(enum.Enum):
+ token = 0
+
+
+#: Can be used as a parameter set id to hide it from the test name.
+HIDDEN_PARAM = _HiddenParam.token
+
+
+def istestfunc(func) -> bool:
+ return callable(func) and getattr(func, "__name__", "") != ""
+
+
+def get_empty_parameterset_mark(
+ config: Config, argnames: Sequence[str], func
+) -> MarkDecorator:
+ from ..nodes import Collector
+
+ argslisting = ", ".join(argnames)
+
+ _fs, lineno = getfslineno(func)
+ reason = f"got empty parameter set for ({argslisting})"
+ requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
+ if requested_mark in ("", None, "skip"):
+ mark = MARK_GEN.skip(reason=reason)
+ elif requested_mark == "xfail":
+ mark = MARK_GEN.xfail(reason=reason, run=False)
+ elif requested_mark == "fail_at_collect":
+ raise Collector.CollectError(
+ f"Empty parameter set in '{func.__name__}' at line {lineno + 1}"
+ )
+ else:
+ raise LookupError(requested_mark)
+ return mark
+
+
+class ParameterSet(NamedTuple):
+ """A set of values for a set of parameters along with associated marks and
+ an optional ID for the set.
+
+ Examples::
+
+ pytest.param(1, 2, 3)
+ # ParameterSet(values=(1, 2, 3), marks=(), id=None)
+
+ pytest.param("hello", id="greeting")
+ # ParameterSet(values=("hello",), marks=(), id="greeting")
+
+ # Parameter set with marks
+ pytest.param(42, marks=pytest.mark.xfail)
+ # ParameterSet(values=(42,), marks=(MarkDecorator(...),), id=None)
+
+ # From parametrize mark (parameter names + list of parameter sets)
+ pytest.mark.parametrize(
+ ("a", "b", "expected"),
+ [
+ (1, 2, 3),
+ pytest.param(40, 2, 42, id="everything"),
+ ],
+ )
+ # ParameterSet(values=(1, 2, 3), marks=(), id=None)
+ # ParameterSet(values=(40, 2, 42), marks=(), id="everything")
+ """
+
+ values: Sequence[object | NotSetType]
+ marks: Collection[MarkDecorator | Mark]
+ id: str | _HiddenParam | None
+
+ @classmethod
+ def param(
+ cls,
+ *values: object,
+ marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
+ id: str | _HiddenParam | None = None,
+ ) -> ParameterSet:
+ if isinstance(marks, MarkDecorator):
+ marks = (marks,)
+ else:
+ assert isinstance(marks, collections.abc.Collection)
+ if any(i.name == "usefixtures" for i in marks):
+ raise ValueError(
+ "pytest.param cannot add pytest.mark.usefixtures; see "
+ "https://docs.pytest.org/en/stable/reference/reference.html#pytest-param"
+ )
+
+ if id is not None:
+ if not isinstance(id, str) and id is not HIDDEN_PARAM:
+ raise TypeError(
+ "Expected id to be a string or a `pytest.HIDDEN_PARAM` sentinel, "
+ f"got {type(id)}: {id!r}",
+ )
+ return cls(values, marks, id)
+
+ @classmethod
+ def extract_from(
+ cls,
+ parameterset: ParameterSet | Sequence[object] | object,
+ force_tuple: bool = False,
+ ) -> ParameterSet:
+ """Extract from an object or objects.
+
+ :param parameterset:
+ A legacy style parameterset that may or may not be a tuple,
+ and may or may not be wrapped into a mess of mark objects.
+
+ :param force_tuple:
+ Enforce tuple wrapping so single argument tuple values
+ don't get decomposed and break tests.
+ """
+ if isinstance(parameterset, cls):
+ return parameterset
+ if force_tuple:
+ return cls.param(parameterset)
+ else:
+ # TODO: Refactor to fix this type-ignore. Currently the following
+ # passes type-checking but crashes:
+ #
+ # @pytest.mark.parametrize(('x', 'y'), [1, 2])
+ # def test_foo(x, y): pass
+ return cls(parameterset, marks=[], id=None) # type: ignore[arg-type]
+
+ @staticmethod
+ def _parse_parametrize_args(
+ argnames: str | Sequence[str],
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
+ *args,
+ **kwargs,
+ ) -> tuple[Sequence[str], bool]:
+ if isinstance(argnames, str):
+ # A trailing comma indicates tuple-style: "arg," is equivalent to ("arg",)
+ # In this case, argvalues should be a list of tuples, not wrapped values.
+ # See https://github.com/pytest-dev/pytest/issues/719
+ has_trailing_comma = argnames.rstrip().endswith(",")
+ argnames = [x.strip() for x in argnames.split(",") if x.strip()]
+ force_tuple = len(argnames) == 1 and not has_trailing_comma
+ else:
+ force_tuple = False
+ return argnames, force_tuple
+
+ @staticmethod
+ def _parse_parametrize_parameters(
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
+ force_tuple: bool,
+ ) -> list[ParameterSet]:
+ return [
+ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
+ ]
+
+ @classmethod
+ def _for_parametrize(
+ cls,
+ argnames: str | Sequence[str],
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
+ func,
+ config: Config,
+ nodeid: str,
+ ) -> tuple[Sequence[str], list[ParameterSet]]:
+ if not isinstance(argvalues, Collection):
+ warnings.warn(
+ PARAMETRIZE_NON_COLLECTION_ITERABLE.format(
+ nodeid=nodeid,
+ type_name=type(argvalues).__name__,
+ ),
+ stacklevel=3,
+ )
+
+ argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
+ parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
+ del argvalues
+
+ if parameters:
+ # Check all parameter sets have the correct number of values.
+ for param in parameters:
+ if len(param.values) != len(argnames):
+ msg = (
+ '{nodeid}: in "parametrize" the number of names ({names_len}):\n'
+ " {names}\n"
+ "must be equal to the number of values ({values_len}):\n"
+ " {values}"
+ )
+ fail(
+ msg.format(
+ nodeid=nodeid,
+ values=param.values,
+ names=argnames,
+ names_len=len(argnames),
+ values_len=len(param.values),
+ ),
+ pytrace=False,
+ )
+ else:
+ # Empty parameter set (likely computed at runtime): create a single
+ # parameter set with NOTSET values, with the "empty parameter set" mark applied to it.
+ mark = get_empty_parameterset_mark(config, argnames, func)
+ parameters.append(
+ ParameterSet(
+ values=(NOTSET,) * len(argnames), marks=[mark], id="NOTSET"
+ )
+ )
+ return argnames, parameters
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class Mark:
+ """A pytest mark."""
+
+ #: Name of the mark.
+ name: str
+ #: Positional arguments of the mark decorator.
+ args: tuple[Any, ...]
+ #: Keyword arguments of the mark decorator.
+ kwargs: Mapping[str, Any]
+
+ #: Source Mark for ids with parametrize Marks.
+ _param_ids_from: Mark | None = dataclasses.field(default=None, repr=False)
+ #: Resolved/generated ids with parametrize Marks.
+ _param_ids_generated: Sequence[str] | None = dataclasses.field(
+ default=None, repr=False
+ )
+
+ def __init__(
+ self,
+ name: str,
+ args: tuple[Any, ...],
+ kwargs: Mapping[str, Any],
+ param_ids_from: Mark | None = None,
+ param_ids_generated: Sequence[str] | None = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ """:meta private:"""
+ check_ispytest(_ispytest)
+ # Weirdness to bypass frozen=True.
+ object.__setattr__(self, "name", name)
+ object.__setattr__(self, "args", args)
+ object.__setattr__(self, "kwargs", kwargs)
+ object.__setattr__(self, "_param_ids_from", param_ids_from)
+ object.__setattr__(self, "_param_ids_generated", param_ids_generated)
+
+ def _has_param_ids(self) -> bool:
+ return "ids" in self.kwargs or len(self.args) >= 4
+
+ def combined_with(self, other: Mark) -> Mark:
+ """Return a new Mark which is a combination of this
+ Mark and another Mark.
+
+ Combines by appending args and merging kwargs.
+
+ :param Mark other: The mark to combine with.
+ :rtype: Mark
+ """
+ assert self.name == other.name
+
+ # Remember source of ids with parametrize Marks.
+ param_ids_from: Mark | None = None
+ if self.name == "parametrize":
+ if other._has_param_ids():
+ param_ids_from = other
+ elif self._has_param_ids():
+ param_ids_from = self
+
+ return Mark(
+ self.name,
+ self.args + other.args,
+ dict(self.kwargs, **other.kwargs),
+ param_ids_from=param_ids_from,
+ _ispytest=True,
+ )
+
+
+# A generic parameter designating an object to which a Mark may
+# be applied -- a test function (callable) or class.
+# Note: a lambda is not allowed, but this can't be represented.
+Markable = TypeVar("Markable", bound=Callable[..., object] | type)
+
+
+@dataclasses.dataclass
+class MarkDecorator:
+ """A decorator for applying a mark on test functions and classes.
+
+ ``MarkDecorators`` are created with ``pytest.mark``::
+
+ mark1 = pytest.mark.NAME # Simple MarkDecorator
+ mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
+
+ and can then be applied as decorators to test functions::
+
+ @mark2
+ def test_function():
+ pass
+
+ When a ``MarkDecorator`` is called, it does the following:
+
+ 1. If called with a single class as its only positional argument and no
+ additional keyword arguments, it attaches the mark to the class so it
+ gets applied automatically to all test cases found in that class.
+
+ 2. If called with a single function as its only positional argument and
+ no additional keyword arguments, it attaches the mark to the function,
+ containing all the arguments already stored internally in the
+ ``MarkDecorator``.
+
+ 3. When called in any other case, it returns a new ``MarkDecorator``
+ instance with the original ``MarkDecorator``'s content updated with
+ the arguments passed to this call.
+
+ Note: The rules above prevent a ``MarkDecorator`` from storing only a
+ single function or class reference as its positional argument with no
+ additional keyword or positional arguments. You can work around this by
+ using `with_args()`.
+ """
+
+ mark: Mark
+
+ def __init__(self, mark: Mark, *, _ispytest: bool = False) -> None:
+ """:meta private:"""
+ check_ispytest(_ispytest)
+ self.mark = mark
+
+ @property
+ def name(self) -> str:
+ """Alias for mark.name."""
+ return self.mark.name
+
+ @property
+ def args(self) -> tuple[Any, ...]:
+ """Alias for mark.args."""
+ return self.mark.args
+
+ @property
+ def kwargs(self) -> Mapping[str, Any]:
+ """Alias for mark.kwargs."""
+ return self.mark.kwargs
+
+ @property
+ def markname(self) -> str:
+ """:meta private:"""
+ return self.name # for backward-compat (2.4.1 had this attr)
+
+ def with_args(self, *args: object, **kwargs: object) -> MarkDecorator:
+ """Return a MarkDecorator with extra arguments added.
+
+ Unlike calling the MarkDecorator, with_args() can be used even
+ if the sole argument is a callable/class.
+ """
+ mark = Mark(self.name, args, kwargs, _ispytest=True)
+ return MarkDecorator(self.mark.combined_with(mark), _ispytest=True)
+
+ # Type ignored because the overloads overlap with an incompatible
+ # return type. Not much we can do about that. Thankfully mypy picks
+ # the first match so it works out even if we break the rules.
+ @overload
+ def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
+ pass
+
+ @overload
+ def __call__(self, *args: object, **kwargs: object) -> MarkDecorator:
+ pass
+
+ def __call__(self, *args: object, **kwargs: object):
+ """Call the MarkDecorator."""
+ if args and not kwargs:
+ func = args[0]
+ is_class = inspect.isclass(func)
+ # For staticmethods/classmethods, the marks are eventually fetched from the
+ # function object, not the descriptor, so unwrap.
+ unwrapped_func = func
+ if isinstance(func, staticmethod | classmethod):
+ unwrapped_func = func.__func__
+ if len(args) == 1 and (istestfunc(unwrapped_func) or is_class):
+ store_mark(unwrapped_func, self.mark)
+ return func
+ return self.with_args(*args, **kwargs)
+
+
+def get_unpacked_marks(
+ obj: object | type,
+ *,
+ consider_mro: bool = True,
+) -> list[Mark]:
+ """Obtain the unpacked marks that are stored on an object.
+
+ If obj is a class and consider_mro is true, return marks applied to
+ this class and all of its super-classes in MRO order. If consider_mro
+ is false, only return marks applied directly to this class.
+ """
+ if isinstance(obj, type):
+ if not consider_mro:
+ mark_lists = [obj.__dict__.get("pytestmark", [])]
+ else:
+ mark_lists = [
+ x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
+ ]
+ mark_list = []
+ for item in mark_lists:
+ if isinstance(item, list):
+ mark_list.extend(item)
+ else:
+ mark_list.append(item)
+ else:
+ mark_attribute = getattr(obj, "pytestmark", [])
+ if mark_attribute is None:
+ warnings.warn(
+ "Module defines a `__getattr__` which returns None for "
+ "'pytestmark' instead of raising AttributeError. "
+ "Make sure `__getattr__` raises AttributeError for "
+ "attributes it does not provide. "
+ "See https://github.com/pytest-dev/pytest/issues/8265",
+ PytestCollectionWarning,
+ stacklevel=2,
+ )
+ mark_list = []
+ elif isinstance(mark_attribute, list):
+ mark_list = mark_attribute
+ else:
+ mark_list = [mark_attribute]
+ return list(normalize_mark_list(mark_list))
+
+
+def normalize_mark_list(
+ mark_list: Iterable[Mark | MarkDecorator],
+) -> Iterable[Mark]:
+ """
+ Normalize an iterable of Mark or MarkDecorator objects into a list of marks
+ by retrieving the `mark` attribute on MarkDecorator instances.
+
+ :param mark_list: marks to normalize
+ :returns: A new list of the extracted Mark objects
+ """
+ for mark in mark_list:
+ mark_obj = getattr(mark, "mark", mark)
+ if not isinstance(mark_obj, Mark):
+ raise TypeError(f"got {mark_obj!r} instead of Mark")
+ yield mark_obj
+
+
+def store_mark(obj, mark: Mark) -> None:
+ """Store a Mark on an object.
+
+ This is used to implement the Mark declarations/decorators correctly.
+ """
+ assert isinstance(mark, Mark), mark
+
+ from ..fixtures import getfixturemarker
+
+ if getfixturemarker(obj) is not None:
+ fail(
+ "Marks cannot be applied to fixtures.\n"
+ "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
+ )
+
+ # Always reassign name to avoid updating pytestmark in a reference that
+ # was only borrowed.
+ obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
+
+
+# Typing for builtin pytest marks. This is cheating; it gives builtin marks
+# special privilege, and breaks modularity. But practicality beats purity...
+if TYPE_CHECKING:
+
+ class _SkipMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,no-overload-impl]
+ def __call__(self, arg: Markable) -> Markable: ...
+
+ @overload
+ def __call__(self, reason: str = ...) -> MarkDecorator: ...
+
+ class _SkipifMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ condition: str | bool = ...,
+ *conditions: str | bool,
+ reason: str = ...,
+ ) -> MarkDecorator: ...
+
+ class _XfailMarkDecorator(MarkDecorator):
+ @overload # type: ignore[override,no-overload-impl]
+ def __call__(self, arg: Markable) -> Markable: ...
+
+ @overload
+ def __call__(
+ self,
+ condition: str | bool = True,
+ *conditions: str | bool,
+ reason: str = ...,
+ run: bool = ...,
+ raises: None
+ | type[BaseException]
+ | tuple[type[BaseException], ...]
+ | AbstractRaises[BaseException] = ...,
+ strict: bool = ...,
+ ) -> MarkDecorator: ...
+
+ class _ParametrizeMarkDecorator(MarkDecorator):
+ def __call__( # type: ignore[override]
+ self,
+ argnames: str | Sequence[str],
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
+ # TODO(pytest10): Change to below after PARAMETRIZE_NON_COLLECTION_ITERABLE deprecation.
+ # Overload doesn't work, see #14606.
+ # argvalues: Collection[ParameterSet | Sequence[object] | object],
+ *,
+ indirect: bool | Sequence[str] = ...,
+ ids: Iterable[None | str | float | int | bool | _HiddenParam]
+ | Callable[[Any], object | None]
+ | None = ...,
+ scope: ScopeName | None = ...,
+ ) -> MarkDecorator: ...
+
+ class _UsefixturesMarkDecorator(MarkDecorator):
+ def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
+ ...
+
+ class _FilterwarningsMarkDecorator(MarkDecorator):
+ def __call__(self, *filters: str) -> MarkDecorator: # type: ignore[override]
+ ...
+
+
+@final
+class MarkGenerator:
+ """Factory for :class:`MarkDecorator` objects - exposed as
+ a ``pytest.mark`` singleton instance.
+
+ Example::
+
+ import pytest
+
+
+ @pytest.mark.slowtest
+ def test_function():
+ pass
+
+ applies a 'slowtest' :class:`Mark` on ``test_function``.
+ """
+
+ # See TYPE_CHECKING above.
+ if TYPE_CHECKING:
+ skip: _SkipMarkDecorator
+ skipif: _SkipifMarkDecorator
+ xfail: _XfailMarkDecorator
+ parametrize: _ParametrizeMarkDecorator
+ usefixtures: _UsefixturesMarkDecorator
+ filterwarnings: _FilterwarningsMarkDecorator
+
+ def __init__(self, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ self._config: Config | None = None
+ self._markers: set[str] = set()
+
+ def __getattr__(self, name: str) -> MarkDecorator:
+ """Generate a new :class:`MarkDecorator` with the given name."""
+ if name[0] == "_":
+ raise AttributeError("Marker name must NOT start with underscore")
+
+ if self._config is not None:
+ # We store a set of markers as a performance optimisation - if a mark
+ # name is in the set we definitely know it, but a mark may be known and
+ # not in the set. We therefore start by updating the set!
+ if name not in self._markers:
+ for line in self._config.getini("markers"):
+ # example lines: "skipif(condition): skip the given test if..."
+ # or "hypothesis: tests which use Hypothesis", so to get the
+ # marker name we split on both `:` and `(`.
+ marker = line.split(":")[0].split("(")[0].strip()
+ self._markers.add(marker)
+
+ # If the name is not in the set of known marks after updating,
+ # then it really is time to issue a warning or an error.
+ if name not in self._markers:
+ # Raise a specific error for common misspellings of "parametrize".
+ if name in ["parameterize", "parametrise", "parameterise"]:
+ __tracebackhide__ = True
+ fail(f"Unknown '{name}' mark, did you mean 'parametrize'?")
+
+ strict_markers = self._config.getini("strict_markers")
+ if strict_markers is None:
+ strict_markers = self._config.getini("strict")
+ if strict_markers:
+ fail(
+ f"{name!r} not found in `markers` configuration option",
+ pytrace=False,
+ )
+
+ warnings.warn(
+ f"Unknown pytest.mark.{name} - is this a typo? You can register "
+ "custom marks to avoid this warning - for details, see "
+ "https://docs.pytest.org/en/stable/how-to/mark.html",
+ PytestUnknownMarkWarning,
+ 2,
+ )
+
+ return MarkDecorator(Mark(name, (), {}, _ispytest=True), _ispytest=True)
+
+
+MARK_GEN = MarkGenerator(_ispytest=True)
+
+
+@final
+class NodeKeywords(MutableMapping[str, Any]):
+ __slots__ = ("_markers", "node", "parent")
+
+ def __init__(self, node: Node) -> None:
+ self.node = node
+ self.parent = node.parent
+ self._markers = {node.name: True}
+
+ def __getitem__(self, key: str) -> Any:
+ try:
+ return self._markers[key]
+ except KeyError:
+ if self.parent is None:
+ raise
+ return self.parent.keywords[key]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self._markers[key] = value
+
+ # Note: we could've avoided explicitly implementing some of the methods
+ # below and use the collections.abc fallback, but that would be slow.
+
+ def __contains__(self, key: object) -> bool:
+ return key in self._markers or (
+ self.parent is not None and key in self.parent.keywords
+ )
+
+ def update( # type: ignore[override]
+ self,
+ other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (),
+ **kwds: Any,
+ ) -> None:
+ self._markers.update(other)
+ self._markers.update(kwds)
+
+ def __delitem__(self, key: str) -> None:
+ raise ValueError("cannot delete key in keywords dict")
+
+ def __iter__(self) -> Iterator[str]:
+ # Doesn't need to be fast.
+ yield from self._markers
+ if self.parent is not None:
+ for keyword in self.parent.keywords:
+ # self._marks and self.parent.keywords can have duplicates.
+ if keyword not in self._markers:
+ yield keyword
+
+ def __len__(self) -> int:
+ # Doesn't need to be fast.
+ return sum(1 for keyword in self)
+
+ def __repr__(self) -> str:
+ return f""
diff --git a/venv/lib/python3.11/site-packages/_pytest/monkeypatch.py b/venv/lib/python3.11/site-packages/_pytest/monkeypatch.py
new file mode 100644
index 000000000..d6db72455
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/monkeypatch.py
@@ -0,0 +1,430 @@
+# mypy: allow-untyped-defs
+"""Monkeypatching and mocking functionality."""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+from collections.abc import Mapping
+from collections.abc import MutableMapping
+from contextlib import contextmanager
+import importlib
+import os
+from pathlib import Path
+import re
+import sys
+from typing import Any
+from typing import final
+from typing import overload
+from typing import TypeVar
+import warnings
+
+from _pytest.compat import NOTSET
+from _pytest.compat import NotSetType
+from _pytest.deprecated import MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES
+from _pytest.fixtures import fixture
+from _pytest.warning_types import PytestWarning
+
+
+RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
+
+
+K = TypeVar("K")
+V = TypeVar("V")
+
+
+@fixture
+def monkeypatch() -> Generator[MonkeyPatch]:
+ """A convenient fixture for monkey-patching.
+
+ The fixture provides these methods to modify objects, dictionaries, or
+ :data:`os.environ`:
+
+ * :meth:`monkeypatch.setattr(obj, name, value, raising=True) `
+ * :meth:`monkeypatch.delattr(obj, name, raising=True) `
+ * :meth:`monkeypatch.setitem(mapping, name, value) `
+ * :meth:`monkeypatch.delitem(obj, name, raising=True) `
+ * :meth:`monkeypatch.setenv(name, value, prepend=None) `
+ * :meth:`monkeypatch.delenv(name, raising=True) `
+ * :meth:`monkeypatch.syspath_prepend(path) `
+ * :meth:`monkeypatch.chdir(path) `
+ * :meth:`monkeypatch.context() `
+
+ All modifications will be undone after the requesting test function or
+ fixture has finished. The ``raising`` parameter determines if a :class:`KeyError`
+ or :class:`AttributeError` will be raised if the set/deletion operation does not have the
+ specified target.
+
+ To undo modifications done by the fixture in a contained scope,
+ use :meth:`context() `.
+ """
+ mpatch = MonkeyPatch()
+ yield mpatch
+ mpatch.undo()
+
+
+def resolve(name: str) -> object:
+ # Simplified from zope.dottedname.
+ parts = name.split(".")
+
+ used = parts.pop(0)
+ found: object = importlib.import_module(used)
+ for part in parts:
+ used += "." + part
+ try:
+ found = getattr(found, part)
+ except AttributeError:
+ pass
+ else:
+ continue
+ # We use explicit un-nesting of the handling block in order
+ # to avoid nested exceptions.
+ try:
+ importlib.import_module(used)
+ except ImportError as ex:
+ expected = str(ex).split()[-1]
+ if expected == used:
+ raise
+ else:
+ raise ImportError(f"import error in {used}: {ex}") from ex
+ found = annotated_getattr(found, part, used)
+ return found
+
+
+def annotated_getattr(obj: object, name: str, ann: str) -> object:
+ try:
+ obj = getattr(obj, name)
+ except AttributeError as e:
+ raise AttributeError(
+ f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}"
+ ) from e
+ return obj
+
+
+def derive_importpath(import_path: str, raising: bool) -> tuple[str, object]:
+ if not isinstance(import_path, str) or "." not in import_path:
+ raise TypeError(f"must be absolute import path string, not {import_path!r}")
+ module, attr = import_path.rsplit(".", 1)
+ target = resolve(module)
+ if raising:
+ annotated_getattr(target, attr, ann=module)
+ return attr, target
+
+
+@final
+class MonkeyPatch:
+ """Helper to conveniently monkeypatch attributes/items/environment
+ variables/syspath.
+
+ Returned by the :fixture:`monkeypatch` fixture.
+
+ .. versionchanged:: 6.2
+ Can now also be used directly as `pytest.MonkeyPatch()`, for when
+ the fixture is not available. In this case, use
+ :meth:`with MonkeyPatch.context() as mp: ` or remember to call
+ :meth:`undo` explicitly.
+ """
+
+ def __init__(self) -> None:
+ self._setattr: list[tuple[object, str, object]] = []
+ self._setitem: list[tuple[Mapping[Any, Any], object, object]] = []
+ self._cwd: str | None = None
+ self._savesyspath: list[str] | None = None
+
+ @classmethod
+ @contextmanager
+ def context(cls) -> Generator[MonkeyPatch]:
+ """Context manager that returns a new :class:`MonkeyPatch` object
+ which undoes any patching done inside the ``with`` block upon exit.
+
+ Example:
+
+ .. code-block:: python
+
+ import functools
+
+
+ def test_partial(monkeypatch):
+ with monkeypatch.context() as m:
+ m.setattr(functools, "partial", 3)
+
+ Useful in situations where it is desired to undo some patches before the test ends,
+ such as mocking ``stdlib`` functions that might break pytest itself if mocked (for examples
+ of this see :issue:`3290`).
+ """
+ m = cls()
+ try:
+ yield m
+ finally:
+ m.undo()
+
+ @overload
+ def setattr(
+ self,
+ target: str,
+ name: object,
+ value: NotSetType = ...,
+ raising: bool = ...,
+ ) -> None: ...
+
+ @overload
+ def setattr(
+ self,
+ target: object,
+ name: str,
+ value: object,
+ raising: bool = ...,
+ ) -> None: ...
+
+ def setattr(
+ self,
+ target: str | object,
+ name: object | str,
+ value: object = NOTSET,
+ raising: bool = True,
+ ) -> None:
+ """
+ Set attribute value on target, memorizing the old value.
+
+ For example:
+
+ .. code-block:: python
+
+ import os
+
+ monkeypatch.setattr(os, "getcwd", lambda: "/")
+
+ The code above replaces the :func:`os.getcwd` function by a ``lambda`` which
+ always returns ``"/"``.
+
+ For convenience, you can specify a string as ``target`` which
+ will be interpreted as a dotted import path, with the last part
+ being the attribute name:
+
+ .. code-block:: python
+
+ monkeypatch.setattr("os.getcwd", lambda: "/")
+
+ Raises :class:`AttributeError` if the attribute does not exist, unless
+ ``raising`` is set to False.
+
+ **Where to patch**
+
+ ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one.
+ There can be many names pointing to any individual object, so for patching to work you must ensure
+ that you patch the name used by the system under test.
+
+ See the section :ref:`Where to patch ` in the :mod:`unittest.mock`
+ docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but
+ applies to ``monkeypatch.setattr`` as well.
+ """
+ __tracebackhide__ = True
+ import inspect
+
+ if value is NOTSET:
+ if not isinstance(target, str):
+ raise TypeError(
+ "use setattr(target, name, value) or "
+ "setattr(target, value) with target being a dotted "
+ "import string"
+ )
+ value = name
+ name, target = derive_importpath(target, raising)
+ else:
+ if not isinstance(name, str):
+ raise TypeError(
+ "use setattr(target, name, value) with name being a string or "
+ "setattr(target, value) with target being a dotted "
+ "import string"
+ )
+
+ oldval = getattr(target, name, NOTSET)
+ if raising and oldval is NOTSET:
+ raise AttributeError(f"{target!r} has no attribute {name!r}")
+
+ # avoid class descriptors like staticmethod/classmethod
+ if inspect.isclass(target):
+ oldval = target.__dict__.get(name, NOTSET)
+ setattr(target, name, value)
+ self._setattr.append((target, name, oldval))
+
+ def delattr(
+ self,
+ target: object | str,
+ name: str | NotSetType = NOTSET,
+ raising: bool = True,
+ ) -> None:
+ """Delete attribute ``name`` from ``target``.
+
+ If no ``name`` is specified and ``target`` is a string
+ it will be interpreted as a dotted import path with the
+ last part being the attribute name.
+
+ Raises AttributeError it the attribute does not exist, unless
+ ``raising`` is set to False.
+ """
+ __tracebackhide__ = True
+ import inspect
+
+ if name is NOTSET:
+ if not isinstance(target, str):
+ raise TypeError(
+ "use delattr(target, name) or "
+ "delattr(target) with target being a dotted "
+ "import string"
+ )
+ name, target = derive_importpath(target, raising)
+
+ if not hasattr(target, name):
+ if raising:
+ raise AttributeError(name)
+ else:
+ oldval = getattr(target, name, NOTSET)
+ # Avoid class descriptors like staticmethod/classmethod.
+ if inspect.isclass(target):
+ oldval = target.__dict__.get(name, NOTSET)
+ self._setattr.append((target, name, oldval))
+ delattr(target, name)
+
+ def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
+ """Set dictionary entry ``name`` to value."""
+ self._setitem.append((dic, name, dic.get(name, NOTSET)))
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ dic[name] = value # type: ignore[index]
+
+ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
+ """Delete ``name`` from dict.
+
+ Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
+ False.
+ """
+ if name not in dic:
+ if raising:
+ raise KeyError(name)
+ else:
+ self._setitem.append((dic, name, dic.get(name, NOTSET)))
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ del dic[name] # type: ignore[attr-defined]
+
+ def setenv(self, name: str, value: str, prepend: str | None = None) -> None:
+ """Set environment variable ``name`` to ``value``.
+
+ If ``prepend`` is a character, read the current environment variable
+ value and prepend the ``value`` adjoined with the ``prepend``
+ character.
+ """
+ if not isinstance(value, str):
+ warnings.warn( # type: ignore[unreachable]
+ PytestWarning(
+ f"Value of environment variable {name} type should be str, but got "
+ f"{value!r} (type: {type(value).__name__}); converted to str implicitly"
+ ),
+ stacklevel=2,
+ )
+ value = str(value)
+ if prepend and name in os.environ:
+ value = value + prepend + os.environ[name]
+ self.setitem(os.environ, name, value)
+
+ def delenv(self, name: str, raising: bool = True) -> None:
+ """Delete ``name`` from the environment.
+
+ Raises ``KeyError`` if it does not exist, unless ``raising`` is set to
+ False.
+ """
+ environ: MutableMapping[str, str] = os.environ
+ self.delitem(environ, name, raising=raising)
+
+ def syspath_prepend(self, path) -> None:
+ """Prepend ``path`` to ``sys.path`` list of import locations."""
+ if self._savesyspath is None:
+ self._savesyspath = sys.path[:]
+ sys.path.insert(0, str(path))
+
+ # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171
+ # this is only needed when pkg_resources was already loaded by the namespace package
+ if "pkg_resources" in sys.modules:
+ import pkg_resources
+ from pkg_resources import fixup_namespace_packages
+
+ # Only issue deprecation warning if this call would actually have an
+ # effect for this specific path.
+ if (
+ hasattr(pkg_resources, "_namespace_packages")
+ and pkg_resources._namespace_packages
+ ):
+ path_obj = Path(str(path))
+ for ns_pkg in pkg_resources._namespace_packages:
+ if ns_pkg is None:
+ continue
+ ns_pkg_path = path_obj / ns_pkg.replace(".", os.sep)
+ if ns_pkg_path.is_dir():
+ warnings.warn(
+ MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES, stacklevel=2
+ )
+ break
+
+ fixup_namespace_packages(str(path))
+
+ # A call to syspathinsert() usually means that the caller wants to
+ # import some dynamically created files, thus with python3 we
+ # invalidate its import caches.
+ # This is especially important when any namespace package is in use,
+ # since then the mtime based FileFinder cache (that gets created in
+ # this case already) gets not invalidated when writing the new files
+ # quickly afterwards.
+ from importlib import invalidate_caches
+
+ invalidate_caches()
+
+ def chdir(self, path: str | os.PathLike[str]) -> None:
+ """Change the current working directory to the specified path.
+
+ :param path:
+ The path to change into.
+ """
+ if self._cwd is None:
+ self._cwd = os.getcwd()
+ os.chdir(path)
+
+ def undo(self) -> None:
+ """Undo previous changes.
+
+ This call consumes the undo stack. Calling it a second time has no
+ effect unless you do more monkeypatching after the undo call.
+
+ There is generally no need to call `undo()`, since it is
+ called automatically during tear-down.
+
+ .. note::
+ The same `monkeypatch` fixture is used across a
+ single test function invocation. If `monkeypatch` is used both by
+ the test function itself and one of the test fixtures,
+ calling `undo()` will undo all of the changes made in
+ both functions.
+
+ Prefer to use :meth:`context() ` instead.
+ """
+ for obj, name, value in reversed(self._setattr):
+ if value is not NOTSET:
+ setattr(obj, name, value)
+ else:
+ delattr(obj, name)
+ self._setattr[:] = []
+ for dictionary, key, value in reversed(self._setitem):
+ if value is NOTSET:
+ try:
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ del dictionary[key] # type: ignore[attr-defined]
+ except KeyError:
+ pass # Was already deleted, so we have the desired state.
+ else:
+ # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
+ dictionary[key] = value # type: ignore[index]
+ self._setitem[:] = []
+ if self._savesyspath is not None:
+ sys.path[:] = self._savesyspath
+ self._savesyspath = None
+
+ if self._cwd is not None:
+ os.chdir(self._cwd)
+ self._cwd = None
diff --git a/venv/lib/python3.11/site-packages/_pytest/nodes.py b/venv/lib/python3.11/site-packages/_pytest/nodes.py
new file mode 100644
index 000000000..f0629c2da
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/nodes.py
@@ -0,0 +1,764 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+import abc
+from collections.abc import Callable
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import MutableMapping
+from functools import cached_property
+from functools import lru_cache
+import os
+import pathlib
+from pathlib import Path
+from typing import Any
+from typing import cast
+from typing import NoReturn
+from typing import overload
+from typing import TYPE_CHECKING
+from typing import TypeVar
+import warnings
+
+import pluggy
+
+import _pytest._code
+from _pytest._code import getfslineno
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest._code.code import Traceback
+from _pytest._code.code import TracebackStyle
+from _pytest.compat import LEGACY_PATH
+from _pytest.compat import signature
+from _pytest.config import Config
+from _pytest.config import ConftestImportFailure
+from _pytest.mark.structures import Mark
+from _pytest.mark.structures import MarkDecorator
+from _pytest.mark.structures import NodeKeywords
+from _pytest.outcomes import fail
+from _pytest.pathlib import absolutepath
+from _pytest.stash import Stash
+from _pytest.warning_types import PytestWarning
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+ # Imported here due to circular import.
+ from _pytest.main import Session
+
+
+SEP = "/"
+
+
+def norm_sep(path: str | os.PathLike[str]) -> str:
+ """Normalize path separators to forward slashes for nodeid compatibility.
+
+ Replaces backslashes with forward slashes. This handles both Windows native
+ paths and cross-platform data (e.g., Windows paths in serialized test reports
+ when running on Linux).
+
+ :param path: A path string or PathLike object.
+ :returns: String with all backslashes replaced by forward slashes.
+ """
+ return os.fspath(path).replace("\\", SEP)
+
+
+tracebackcutdir = Path(_pytest.__file__).parent
+
+
+_T = TypeVar("_T")
+
+
+_NodeType = TypeVar("_NodeType", bound="Node")
+
+
+class NodeMeta(abc.ABCMeta):
+ """Metaclass used by :class:`Node` to enforce that direct construction raises
+ :class:`Failed`.
+
+ This behaviour supports the indirection introduced with :meth:`Node.from_parent`,
+ the named constructor to be used instead of direct construction. The design
+ decision to enforce indirection with :class:`NodeMeta` was made as a
+ temporary aid for refactoring the collection tree, which was diagnosed to
+ have :class:`Node` objects whose creational patterns were overly entangled.
+ Once the refactoring is complete, this metaclass can be removed.
+
+ See https://github.com/pytest-dev/pytest/projects/3 for an overview of the
+ progress on detangling the :class:`Node` classes.
+ """
+
+ def __call__(cls, *k, **kw) -> NoReturn:
+ msg = (
+ "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
+ "See "
+ "https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
+ " for more details."
+ ).format(name=f"{cls.__module__}.{cls.__name__}")
+ fail(msg, pytrace=False)
+
+ def _create(cls: type[_T], *k, **kw) -> _T:
+ try:
+ return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
+ except TypeError:
+ sig = signature(getattr(cls, "__init__"))
+ known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
+ from .warning_types import PytestDeprecationWarning
+
+ warnings.warn(
+ PytestDeprecationWarning(
+ f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
+ "See https://docs.pytest.org/en/stable/deprecations.html"
+ "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
+ "for more details."
+ )
+ )
+
+ return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc]
+
+
+class Node(abc.ABC, metaclass=NodeMeta):
+ r"""Base class of :class:`Collector` and :class:`Item`, the components of
+ the test collection tree.
+
+ ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
+ leaf nodes.
+ """
+
+ # Implemented in the legacypath plugin.
+ #: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
+ #: for methods not migrated to ``pathlib.Path`` yet, such as
+ #: :meth:`Item.reportinfo `. Will be deprecated in
+ #: a future release, prefer using :attr:`path` instead.
+ fspath: LEGACY_PATH
+
+ # Use __slots__ to make attribute access faster.
+ # Note that __dict__ is still available.
+ __slots__ = (
+ "__dict__",
+ "_nodeid",
+ "_store",
+ "config",
+ "name",
+ "parent",
+ "path",
+ "session",
+ )
+
+ def __init__(
+ self,
+ name: str,
+ parent: Node | None = None,
+ config: Config | None = None,
+ session: Session | None = None,
+ fspath: None = None,
+ path: Path | None = None,
+ nodeid: str | None = None,
+ ) -> None:
+ #: A unique name within the scope of the parent node.
+ self.name: str = name
+
+ #: The parent collector node.
+ self.parent = parent
+
+ if config:
+ #: The pytest config object.
+ self.config: Config = config
+ else:
+ if not parent:
+ raise TypeError("config or parent must be provided")
+ self.config = parent.config
+
+ if session:
+ #: The pytest session this node is part of.
+ self.session: Session = session
+ else:
+ if not parent:
+ raise TypeError("session or parent must be provided")
+ self.session = parent.session
+
+ if path is None:
+ assert parent is not None
+ path = parent.path
+ #: Filesystem path where this node was collected from.
+ self.path: pathlib.Path = path
+
+ # The explicit annotation is to avoid publicly exposing NodeKeywords.
+ #: Keywords/markers collected from all scopes.
+ self.keywords: MutableMapping[str, Any] = NodeKeywords(self)
+
+ #: The marker objects belonging to this node.
+ self.own_markers: list[Mark] = []
+
+ #: Allow adding of extra keywords to use for matching.
+ self.extra_keyword_matches: set[str] = set()
+
+ if nodeid is not None:
+ assert "::()" not in nodeid
+ self._nodeid = nodeid
+ else:
+ if not self.parent:
+ raise TypeError("nodeid or parent must be provided")
+ self._nodeid = self.parent.nodeid + "::" + self.name
+
+ #: A place where plugins can store information on the node for their
+ #: own use.
+ self.stash: Stash = Stash()
+ # Deprecated alias. Was never public. Can be removed in a few releases.
+ self._store = self.stash
+
+ @classmethod
+ def from_parent(cls, parent: Node, **kw) -> Self:
+ """Public constructor for Nodes.
+
+ This indirection got introduced in order to enable removing
+ the fragile logic from the node constructors.
+
+ Subclasses can use ``super().from_parent(...)`` when overriding the
+ construction.
+
+ :param parent: The parent node of this Node.
+ """
+ if "config" in kw:
+ raise TypeError("config is not a valid argument for from_parent")
+ if "session" in kw:
+ raise TypeError("session is not a valid argument for from_parent")
+ return cls._create(parent=parent, **kw)
+
+ @property
+ def ihook(self) -> pluggy.HookRelay:
+ """Path-sensitive hook proxy used to call pytest hooks."""
+ return self.session.gethookproxy(self.path)
+
+ def __repr__(self) -> str:
+ return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None))
+
+ def warn(self, warning: Warning) -> None:
+ """Issue a warning for this Node.
+
+ Warnings will be displayed after the test session, unless explicitly suppressed.
+
+ :param Warning warning:
+ The warning instance to issue.
+
+ :raises ValueError: If ``warning`` instance is not a subclass of Warning.
+
+ Example usage:
+
+ .. code-block:: python
+
+ node.warn(PytestWarning("some message"))
+ node.warn(UserWarning("some message"))
+
+ .. versionchanged:: 6.2
+ Any subclass of :class:`Warning` is now accepted, rather than only
+ :class:`PytestWarning ` subclasses.
+ """
+ # enforce type checks here to avoid getting a generic type error later otherwise.
+ if not isinstance(warning, Warning):
+ raise ValueError(
+ f"warning must be an instance of Warning or subclass, got {warning!r}"
+ )
+ path, lineno = get_fslocation_from_item(self)
+ assert lineno is not None
+ warnings.warn_explicit(
+ warning,
+ category=None,
+ filename=str(path),
+ lineno=lineno + 1,
+ )
+
+ # Methods for ordering nodes.
+
+ @property
+ def nodeid(self) -> str:
+ """A ::-separated string denoting its collection tree address."""
+ return self._nodeid
+
+ def __hash__(self) -> int:
+ return hash(self._nodeid)
+
+ def setup(self) -> None:
+ pass
+
+ def teardown(self) -> None:
+ pass
+
+ def iter_parents(self) -> Iterator[Node]:
+ """Iterate over all parent collectors starting from and including self
+ up to the root of the collection tree.
+
+ .. versionadded:: 8.1
+ """
+ parent: Node | None = self
+ while parent is not None:
+ yield parent
+ parent = parent.parent
+
+ def listchain(self) -> list[Node]:
+ """Return a list of all parent collectors starting from the root of the
+ collection tree down to and including self."""
+ chain = []
+ item: Node | None = self
+ while item is not None:
+ chain.append(item)
+ item = item.parent
+ chain.reverse()
+ return chain
+
+ def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None:
+ """Dynamically add a marker object to the node.
+
+ :param marker:
+ The marker.
+ :param append:
+ Whether to append the marker, or prepend it.
+ """
+ from _pytest.mark import MARK_GEN
+
+ if isinstance(marker, MarkDecorator):
+ marker_ = marker
+ elif isinstance(marker, str):
+ marker_ = getattr(MARK_GEN, marker)
+ else:
+ raise ValueError("is not a string or pytest.mark.* Marker")
+ self.keywords[marker_.name] = marker_
+ if append:
+ self.own_markers.append(marker_.mark)
+ else:
+ self.own_markers.insert(0, marker_.mark)
+
+ def iter_markers(self, name: str | None = None) -> Iterator[Mark]:
+ """Iterate over all markers of the node.
+
+ :param name: If given, filter the results by the name attribute.
+ :returns: An iterator of the markers of the node.
+ """
+ return (x[1] for x in self.iter_markers_with_node(name=name))
+
+ def iter_markers_with_node(
+ self, name: str | None = None
+ ) -> Iterator[tuple[Node, Mark]]:
+ """Iterate over all markers of the node.
+
+ :param name: If given, filter the results by the name attribute.
+ :returns: An iterator of (node, mark) tuples.
+ """
+ for node in self.iter_parents():
+ for mark in node.own_markers:
+ if name is None or getattr(mark, "name", None) == name:
+ yield node, mark
+
+ @overload
+ def get_closest_marker(self, name: str) -> Mark | None: ...
+
+ @overload
+ def get_closest_marker(self, name: str, default: Mark) -> Mark: ...
+
+ def get_closest_marker(self, name: str, default: Mark | None = None) -> Mark | None:
+ """Return the first marker matching the name, from closest (for
+ example function) to farther level (for example module level).
+
+ :param default: Fallback return value if no marker was found.
+ :param name: Name to filter by.
+ """
+ return next(self.iter_markers(name=name), default)
+
+ def listextrakeywords(self) -> set[str]:
+ """Return a set of all extra keywords in self and any parents."""
+ extra_keywords: set[str] = set()
+ for item in self.listchain():
+ extra_keywords.update(item.extra_keyword_matches)
+ return extra_keywords
+
+ def listnames(self) -> list[str]:
+ return [x.name for x in self.listchain()]
+
+ def addfinalizer(self, fin: Callable[[], object]) -> None:
+ """Register a function to be called without arguments when this node is
+ finalized.
+
+ This method can only be called when this node is active
+ in a setup chain, for example during self.setup().
+ """
+ self.session._setupstate.addfinalizer(fin, self)
+
+ def getparent(self, cls: type[_NodeType]) -> _NodeType | None:
+ """Get the closest parent node (including self) which is an instance of
+ the given class.
+
+ :param cls: The node class to search for.
+ :returns: The node, if found.
+ """
+ for node in self.iter_parents():
+ if isinstance(node, cls):
+ return node
+ return None
+
+ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
+ return excinfo.traceback
+
+ def _repr_failure_py(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ style: TracebackStyle | None = None,
+ ) -> TerminalRepr:
+ from _pytest.fixtures import FixtureLookupError
+
+ if isinstance(excinfo.value, ConftestImportFailure):
+ excinfo = ExceptionInfo.from_exception(excinfo.value.cause)
+ if isinstance(excinfo.value, fail.Exception):
+ if not excinfo.value.pytrace:
+ style = "value"
+ if isinstance(excinfo.value, FixtureLookupError):
+ return excinfo.value.formatrepr()
+
+ tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback]
+ if self.config.getoption("fulltrace", False):
+ style = "long"
+ tbfilter = False
+ else:
+ tbfilter = self._traceback_filter
+ if style == "auto":
+ style = "long"
+ # XXX should excinfo.getrepr record all data and toterminal() process it?
+ if style is None:
+ if self.config.getoption("tbstyle", "auto") == "short":
+ style = "short"
+ else:
+ style = "long"
+
+ if self.config.get_verbosity() > 1:
+ truncate_locals = False
+ else:
+ truncate_locals = True
+
+ truncate_args = False if self.config.get_verbosity() > 2 else True
+
+ # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False.
+ # It is possible for a fixture/test to change the CWD while this code runs, which
+ # would then result in the user seeing confusing paths in the failure message.
+ # To fix this, if the CWD changed, always display the full absolute path.
+ # It will be better to just always display paths relative to invocation_dir, but
+ # this requires a lot of plumbing (#6428).
+ try:
+ abspath = Path(os.getcwd()) != self.config.invocation_params.dir
+ except OSError:
+ abspath = True
+
+ return excinfo.getrepr(
+ funcargs=True,
+ abspath=abspath,
+ showlocals=self.config.getoption("showlocals", False),
+ style=style,
+ tbfilter=tbfilter,
+ truncate_locals=truncate_locals,
+ truncate_args=truncate_args,
+ )
+
+ def repr_failure(
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ style: TracebackStyle | None = None,
+ ) -> str | TerminalRepr:
+ """Return a representation of a collection or test failure.
+
+ .. seealso:: :ref:`non-python tests`
+
+ :param excinfo: Exception information for the failure.
+ """
+ return self._repr_failure_py(excinfo, style)
+
+
+def get_fslocation_from_item(node: Node) -> tuple[str | Path, int | None]:
+ """Try to extract the actual location from a node, depending on available attributes:
+
+ * "location": a pair (path, lineno)
+ * "obj": a Python object that the node wraps.
+ * "path": just a path
+
+ :rtype: A tuple of (str|Path, int) with filename and 0-based line number.
+ """
+ # See Item.location.
+ location: tuple[str, int | None, str] | None = getattr(node, "location", None)
+ if location is not None:
+ return location[:2]
+ obj = getattr(node, "obj", None)
+ if obj is not None:
+ return getfslineno(obj)
+ return getattr(node, "path", "unknown location"), -1
+
+
+class Collector(Node, abc.ABC):
+ """Base class of all collectors.
+
+ Collector create children through `collect()` and thus iteratively build
+ the collection tree.
+ """
+
+ class CollectError(Exception):
+ """An error during collection, contains a custom message."""
+
+ @abc.abstractmethod
+ def collect(self) -> Iterable[Item | Collector]:
+ """Collect children (items and collectors) for this collector."""
+ raise NotImplementedError("abstract")
+
+ # TODO: This omits the style= parameter which breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self, excinfo: ExceptionInfo[BaseException]
+ ) -> str | TerminalRepr:
+ """Return a representation of a collection failure.
+
+ :param excinfo: Exception information for the failure.
+ """
+ if isinstance(excinfo.value, self.CollectError) and not self.config.getoption(
+ "fulltrace", False
+ ):
+ exc = excinfo.value
+ return str(exc.args[0])
+
+ # Respect explicit tbstyle option, but default to "short"
+ # (_repr_failure_py uses "long" with "fulltrace" option always).
+ tbstyle = self.config.getoption("tbstyle", "auto")
+ if tbstyle == "auto":
+ tbstyle = "short"
+
+ return self._repr_failure_py(excinfo, style=tbstyle)
+
+ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
+ if hasattr(self, "path"):
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=self.path)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
+ return ntraceback.filter(excinfo)
+ return excinfo.traceback
+
+
+@lru_cache(maxsize=1000)
+def _check_initialpaths_for_relpath(
+ initial_paths: frozenset[Path], path: Path
+) -> str | None:
+ if path in initial_paths:
+ return ""
+
+ for parent in path.parents:
+ if parent in initial_paths:
+ return str(path.relative_to(parent))
+
+ return None
+
+
+class FSCollector(Collector, abc.ABC):
+ """Base class for filesystem collectors."""
+
+ def __init__(
+ self,
+ fspath: None = None,
+ path_or_parent: Path | Node | None = None,
+ path: Path | None = None,
+ name: str | None = None,
+ parent: Node | None = None,
+ config: Config | None = None,
+ session: Session | None = None,
+ nodeid: str | None = None,
+ ) -> None:
+ if path_or_parent:
+ if isinstance(path_or_parent, Node):
+ assert parent is None
+ parent = cast(FSCollector, path_or_parent)
+ elif isinstance(path_or_parent, Path):
+ assert path is None
+ path = path_or_parent
+ assert path is not None
+
+ if name is None:
+ name = path.name
+ if parent is not None and parent.path != path:
+ try:
+ rel = path.relative_to(parent.path)
+ except ValueError:
+ pass
+ else:
+ name = str(rel)
+ name = norm_sep(name)
+ self.path = path
+
+ if session is None:
+ assert parent is not None
+ session = parent.session
+
+ if nodeid is None:
+ try:
+ nodeid = str(self.path.relative_to(session.config.rootpath))
+ except ValueError:
+ nodeid = _check_initialpaths_for_relpath(session._initialpaths, path)
+
+ if nodeid:
+ nodeid = norm_sep(nodeid)
+
+ super().__init__(
+ name=name,
+ parent=parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ path=path,
+ )
+
+ @classmethod
+ def from_parent(
+ cls,
+ parent,
+ *,
+ fspath: None = None,
+ path: Path | None = None,
+ **kw,
+ ) -> Self:
+ """The public constructor."""
+ return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
+
+
+class File(FSCollector, abc.ABC):
+ """Base class for collecting tests from a file.
+
+ :ref:`non-python tests`.
+ """
+
+
+class Directory(FSCollector, abc.ABC):
+ """Base class for collecting files from a directory.
+
+ A basic directory collector does the following: goes over the files and
+ sub-directories in the directory and creates collectors for them by calling
+ the hooks :hook:`pytest_collect_directory` and :hook:`pytest_collect_file`,
+ after checking that they are not ignored using
+ :hook:`pytest_ignore_collect`.
+
+ The default directory collectors are :class:`~pytest.Dir` and
+ :class:`~pytest.Package`.
+
+ .. versionadded:: 8.0
+
+ :ref:`custom directory collectors`.
+ """
+
+
+class Item(Node, abc.ABC):
+ """Base class of all test invocation items.
+
+ Note that for a single function there might be multiple test invocation items.
+ """
+
+ nextitem = None
+
+ def __init__(
+ self,
+ name,
+ parent=None,
+ config: Config | None = None,
+ session: Session | None = None,
+ nodeid: str | None = None,
+ **kw,
+ ) -> None:
+ # The first two arguments are intentionally passed positionally,
+ # to keep plugins who define a node type which inherits from
+ # (pytest.Item, pytest.File) working (see issue #8435).
+ # They can be made kwargs when the deprecation above is done.
+ super().__init__(
+ name,
+ parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ **kw,
+ )
+ self._report_sections: list[tuple[str, str, str]] = []
+
+ #: A list of tuples (name, value) that holds user defined properties
+ #: for this test.
+ self.user_properties: list[tuple[str, object]] = []
+
+ self._check_item_and_collector_diamond_inheritance()
+
+ def _check_item_and_collector_diamond_inheritance(self) -> None:
+ """
+ Check if the current type inherits from both File and Collector
+ at the same time, emitting a warning accordingly (#8447).
+ """
+ cls = type(self)
+
+ # We inject an attribute in the type to avoid issuing this warning
+ # for the same class more than once, which is not helpful.
+ # It is a hack, but was deemed acceptable in order to avoid
+ # flooding the user in the common case.
+ attr_name = "_pytest_diamond_inheritance_warning_shown"
+ if getattr(cls, attr_name, False):
+ return
+ setattr(cls, attr_name, True)
+
+ problems = ", ".join(
+ base.__name__ for base in cls.__bases__ if issubclass(base, Collector)
+ )
+ if problems:
+ warnings.warn(
+ f"{cls.__name__} is an Item subclass and should not be a collector, "
+ f"however its bases {problems} are collectors.\n"
+ "Please split the Collectors and the Item into separate node types.\n"
+ "Pytest Doc example: https://docs.pytest.org/en/latest/example/nonpython.html\n"
+ "example pull request on a plugin: https://github.com/asmeurer/pytest-flakes/pull/40/",
+ PytestWarning,
+ )
+
+ @abc.abstractmethod
+ def runtest(self) -> None:
+ """Run the test case for this item.
+
+ Must be implemented by subclasses.
+
+ .. seealso:: :ref:`non-python tests`
+ """
+ raise NotImplementedError("runtest must be implemented by Item subclass")
+
+ def add_report_section(self, when: str, key: str, content: str) -> None:
+ """Add a new report section, similar to what's done internally to add
+ stdout and stderr captured output::
+
+ item.add_report_section("call", "stdout", "report section contents")
+
+ :param str when:
+ One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
+ :param str key:
+ Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
+ ``"stderr"`` internally.
+ :param str content:
+ The full contents as a string.
+ """
+ if content:
+ self._report_sections.append((when, key, content))
+
+ def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
+ """Get location information for this item for test reports.
+
+ Returns a tuple with three elements:
+
+ - The path of the test (default ``self.path``)
+ - The 0-based line number of the test (default ``None``)
+ - A name of the test to be shown (default ``""``)
+
+ .. seealso:: :ref:`non-python tests`
+ """
+ return self.path, None, ""
+
+ @cached_property
+ def location(self) -> tuple[str, int | None, str]:
+ """
+ Returns a tuple of ``(relfspath, lineno, testname)`` for this item
+ where ``relfspath`` is file path relative to ``config.rootpath``
+ and lineno is a 0-based line number.
+ """
+ location = self.reportinfo()
+ path = absolutepath(location[0])
+ relfspath = self.session._node_location_to_relpath(path)
+ assert type(location[2]) is str
+ return (relfspath, location[1], location[2])
diff --git a/venv/lib/python3.11/site-packages/_pytest/outcomes.py b/venv/lib/python3.11/site-packages/_pytest/outcomes.py
new file mode 100644
index 000000000..257b95cf5
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/outcomes.py
@@ -0,0 +1,286 @@
+"""Exception classes and constants handling test outcomes as well as
+functions creating them."""
+
+from __future__ import annotations
+
+import importlib
+import sys
+from typing import Any
+from typing import ClassVar
+from typing import NoReturn
+
+
+class OutcomeException(BaseException):
+ """OutcomeException and its subclass instances indicate and contain info
+ about test and collection outcomes."""
+
+ def __init__(self, msg: str | None = None, pytrace: bool = True) -> None:
+ if msg is not None and not isinstance(msg, str):
+ error_msg = ( # type: ignore[unreachable]
+ "{} expected string as 'msg' parameter, got '{}' instead.\n"
+ "Perhaps you meant to use a mark?"
+ )
+ raise TypeError(error_msg.format(type(self).__name__, type(msg).__name__))
+ super().__init__(msg)
+ self.msg = msg
+ self.pytrace = pytrace
+
+ def __repr__(self) -> str:
+ if self.msg is not None:
+ return self.msg
+ return f"<{self.__class__.__name__} instance>"
+
+ __str__ = __repr__
+
+
+TEST_OUTCOME = (OutcomeException, Exception)
+
+
+class Skipped(OutcomeException):
+ # XXX hackish: on 3k we fake to live in the builtins
+ # in order to have Skipped exception printing shorter/nicer
+ __module__ = "builtins"
+
+ def __init__(
+ self,
+ msg: str | None = None,
+ pytrace: bool = True,
+ allow_module_level: bool = False,
+ *,
+ _use_item_location: bool = False,
+ ) -> None:
+ super().__init__(msg=msg, pytrace=pytrace)
+ self.allow_module_level = allow_module_level
+ # If true, the skip location is reported as the item's location,
+ # instead of the place that raises the exception/calls skip().
+ self._use_item_location = _use_item_location
+
+
+class Failed(OutcomeException):
+ """Raised from an explicit call to pytest.fail()."""
+
+ __module__ = "builtins"
+
+
+class Exit(Exception):
+ """Raised for immediate program exits (no tracebacks/summaries)."""
+
+ def __init__(
+ self, msg: str = "unknown reason", returncode: int | None = None
+ ) -> None:
+ self.msg = msg
+ self.returncode = returncode
+ super().__init__(msg)
+
+
+class XFailed(Failed):
+ """Raised from an explicit call to pytest.xfail()."""
+
+
+class _Exit:
+ """Exit testing process.
+
+ :param reason:
+ The message to show as the reason for exiting pytest. reason has a default value
+ only because `msg` is deprecated.
+
+ :param returncode:
+ Return code to be used when exiting pytest. None means the same as ``0`` (no error),
+ same as :func:`sys.exit`.
+
+ :raises pytest.exit.Exception:
+ The exception that is raised.
+ """
+
+ Exception: ClassVar[type[Exit]] = Exit
+
+ def __call__(self, reason: str = "", returncode: int | None = None) -> NoReturn:
+ __tracebackhide__ = True
+ raise Exit(msg=reason, returncode=returncode)
+
+
+exit: _Exit = _Exit()
+
+
+class _Skip:
+ """Skip an executing test with the given message.
+
+ This function should be called only during testing (setup, call or teardown) or
+ during collection by using the ``allow_module_level`` flag. This function can
+ be called in doctests as well.
+
+ :param reason:
+ The message to show the user as reason for the skip.
+
+ :param allow_module_level:
+ Allows this function to be called at module level.
+ Raising the skip exception at module level will stop
+ the execution of the module and prevent the collection of all tests in the module,
+ even those defined before the `skip` call.
+
+ Defaults to False.
+
+ :raises pytest.skip.Exception:
+ The exception that is raised.
+
+ .. note::
+ It is better to use the :ref:`pytest.mark.skipif ref` marker when
+ possible to declare a test to be skipped under certain conditions
+ like mismatching platforms or dependencies.
+ Similarly, use the ``# doctest: +SKIP`` directive (see :py:data:`doctest.SKIP`)
+ to skip a doctest statically.
+ """
+
+ Exception: ClassVar[type[Skipped]] = Skipped
+
+ def __call__(self, reason: str = "", allow_module_level: bool = False) -> NoReturn:
+ __tracebackhide__ = True
+ raise Skipped(msg=reason, allow_module_level=allow_module_level)
+
+
+skip: _Skip = _Skip()
+
+
+class _Fail:
+ """Explicitly fail an executing test with the given message.
+
+ :param reason:
+ The message to show the user as reason for the failure.
+
+ :param pytrace:
+ If False, msg represents the full failure information and no
+ python traceback will be reported.
+
+ :raises pytest.fail.Exception:
+ The exception that is raised.
+ """
+
+ Exception: ClassVar[type[Failed]] = Failed
+
+ def __call__(self, reason: str = "", pytrace: bool = True) -> NoReturn:
+ __tracebackhide__ = True
+ raise Failed(msg=reason, pytrace=pytrace)
+
+
+fail: _Fail = _Fail()
+
+
+class _XFail:
+ """Imperatively xfail an executing test or setup function with the given reason.
+
+ This function should be called only during testing (setup, call or teardown).
+
+ No other code is executed after using ``xfail()`` (it is implemented
+ internally by raising an exception).
+
+ :param reason:
+ The message to show the user as reason for the xfail.
+
+ .. note::
+ It is better to use the :ref:`pytest.mark.xfail ref` marker when
+ possible to declare a test to be xfailed under certain conditions
+ like known bugs or missing features.
+
+ :raises pytest.xfail.Exception:
+ The exception that is raised.
+ """
+
+ Exception: ClassVar[type[XFailed]] = XFailed
+
+ def __call__(self, reason: str = "") -> NoReturn:
+ __tracebackhide__ = True
+ raise XFailed(msg=reason)
+
+
+xfail: _XFail = _XFail()
+
+
+def importorskip(
+ modname: str,
+ minversion: str | None = None,
+ reason: str | None = None,
+ *,
+ exc_type: type[ImportError] | None = None,
+) -> Any:
+ """Import and return the requested module ``modname``, or skip the
+ current test if the module cannot be imported.
+
+ :param modname:
+ The name of the module to import.
+ :param minversion:
+ If given, the imported module's ``__version__`` attribute must be at
+ least this minimal version, otherwise the test is still skipped.
+ :param reason:
+ If given, this reason is shown as the message when the module cannot
+ be imported.
+ :param exc_type:
+ The exception that should be captured in order to skip modules.
+ Must be :py:class:`ImportError` or a subclass.
+
+ Defaults to :class:`ModuleNotFoundError` when not given, which means
+ the module must be missing for the test to be skipped.
+ Pass ``exc_type=ImportError`` to also skip modules that raise
+ :class:`ImportError` during import.
+
+ See :ref:`import-or-skip-import-error` for details.
+
+
+ :returns:
+ The imported module. This should be assigned to its canonical name.
+
+ :raises pytest.skip.Exception:
+ If the module cannot be imported.
+
+ Example::
+
+ docutils = pytest.importorskip("docutils")
+
+ .. versionadded:: 8.2
+
+ The ``exc_type`` parameter.
+
+ .. versionchanged:: 9.1
+
+ The default for ``exc_type`` is now :class:`ModuleNotFoundError`.
+ """
+ import warnings
+
+ __tracebackhide__ = True
+ compile(modname, "", "eval") # to catch syntaxerrors
+
+ # Keep the public signature compatible while using the pytest 9.1 default behavior.
+ if exc_type is None:
+ exc_type = ModuleNotFoundError
+
+ skipped: Skipped | None = None
+
+ with warnings.catch_warnings():
+ # Make sure to ignore ImportWarnings that might happen because
+ # of existing directories with the same name we're trying to
+ # import but without a __init__.py file.
+ warnings.simplefilter("ignore")
+
+ try:
+ importlib.import_module(modname)
+ except exc_type as exc:
+ # Do not raise or issue warnings inside the catch_warnings() block.
+ if reason is None:
+ reason = f"could not import {modname!r}: {exc}"
+ skipped = Skipped(reason, allow_module_level=True)
+ if skipped:
+ raise skipped
+
+ mod = sys.modules[modname]
+ if minversion is None:
+ return mod
+ verattr = getattr(mod, "__version__", None)
+ if minversion is not None:
+ # Imported lazily to improve start-up time.
+ from packaging.version import Version
+
+ if verattr is None or Version(verattr) < Version(minversion):
+ raise Skipped(
+ f"module {modname!r} has __version__ {verattr!r}, required is: {minversion!r}",
+ allow_module_level=True,
+ )
+ return mod
diff --git a/venv/lib/python3.11/site-packages/_pytest/pastebin.py b/venv/lib/python3.11/site-packages/_pytest/pastebin.py
new file mode 100644
index 000000000..e6a143022
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/pastebin.py
@@ -0,0 +1,121 @@
+# mypy: allow-untyped-defs
+"""Submit failure or test session information to a pastebin service."""
+
+from __future__ import annotations
+
+from io import StringIO
+import tempfile
+from typing import IO
+
+from _pytest.config import Config
+from _pytest.config import create_terminal_writer
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import PASTEBIN
+from _pytest.stash import StashKey
+from _pytest.terminal import TerminalReporter
+import pytest
+
+
+pastebinfile_key = StashKey[IO[bytes]]()
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting")
+ group.addoption(
+ "--pastebin",
+ metavar="mode",
+ action="store",
+ dest="pastebin",
+ default=None,
+ choices=["failed", "all"],
+ help="Send failed|all info to bpaste.net pastebin service",
+ )
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_configure(config: Config) -> None:
+ if config.option.pastebin:
+ config.issue_config_time_warning(PASTEBIN, 2)
+
+ if config.option.pastebin == "all":
+ tr = config.pluginmanager.getplugin("terminalreporter")
+ # If no terminal reporter plugin is present, nothing we can do here;
+ # this can happen when this function executes in a worker node
+ # when using pytest-xdist, for example.
+ if tr is not None:
+ # pastebin file will be UTF-8 encoded binary file.
+ config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b")
+ oldwrite = tr._tw.write
+
+ def tee_write(s, **kwargs):
+ oldwrite(s, **kwargs)
+ if isinstance(s, str):
+ s = s.encode("utf-8")
+ config.stash[pastebinfile_key].write(s)
+
+ tr._tw.write = tee_write
+
+
+def pytest_unconfigure(config: Config) -> None:
+ if pastebinfile_key in config.stash:
+ pastebinfile = config.stash[pastebinfile_key]
+ # Get terminal contents and delete file.
+ pastebinfile.seek(0)
+ sessionlog = pastebinfile.read()
+ pastebinfile.close()
+ del config.stash[pastebinfile_key]
+ # Undo our patching in the terminal reporter.
+ tr = config.pluginmanager.getplugin("terminalreporter")
+ del tr._tw.__dict__["write"]
+ # Write summary.
+ tr.write_sep("=", "Sending information to Paste Service")
+ pastebinurl = create_new_paste(sessionlog)
+ tr.write_line(f"pastebin session-log: {pastebinurl}\n")
+
+
+def create_new_paste(contents: str | bytes) -> str:
+ """Create a new paste using the bpaste.net service.
+
+ :contents: Paste contents string.
+ :returns: URL to the pasted contents, or an error message.
+ """
+ import re
+ from urllib.error import HTTPError
+ from urllib.parse import urlencode
+ from urllib.request import urlopen
+
+ params = {"code": contents, "lexer": "text", "expiry": "1week"}
+ url = "https://bpa.st"
+ try:
+ response: str = (
+ urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
+ )
+ except HTTPError as e:
+ with e: # HTTPErrors are also http responses that must be closed!
+ return f"bad response: {e}"
+ except OSError as e: # eg urllib.error.URLError
+ return f"bad response: {e}"
+ m = re.search(r'href="/raw/(\w+)"', response)
+ if m:
+ return f"{url}/show/{m.group(1)}"
+ else:
+ return "bad response: invalid format ('" + response + "')"
+
+
+def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
+ if terminalreporter.config.option.pastebin != "failed":
+ return
+ if "failed" in terminalreporter.stats:
+ terminalreporter.write_sep("=", "Sending information to Paste Service")
+ for rep in terminalreporter.stats["failed"]:
+ try:
+ msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
+ except AttributeError:
+ msg = terminalreporter._getfailureheadline(rep)
+ file = StringIO()
+ tw = create_terminal_writer(terminalreporter.config, file)
+ rep.toterminal(tw)
+ s = file.getvalue()
+ assert len(s)
+ pastebinurl = create_new_paste(s)
+ terminalreporter.write_line(f"{msg} --> {pastebinurl}")
diff --git a/venv/lib/python3.11/site-packages/_pytest/pathlib.py b/venv/lib/python3.11/site-packages/_pytest/pathlib.py
new file mode 100644
index 000000000..291bdf4ec
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/pathlib.py
@@ -0,0 +1,1054 @@
+from __future__ import annotations
+
+from collections.abc import Callable
+from collections.abc import Iterable
+from collections.abc import Iterator
+import contextlib
+from enum import Enum
+from errno import EBADF
+from errno import ELOOP
+from errno import ENOENT
+from errno import ENOTDIR
+import fnmatch
+from functools import partial
+from importlib.machinery import ModuleSpec
+from importlib.machinery import PathFinder
+import importlib.util
+import itertools
+import os
+from os.path import expanduser
+from os.path import expandvars
+from os.path import isabs
+from os.path import sep
+from pathlib import Path
+from pathlib import PurePath
+from posixpath import sep as posix_sep
+import shutil
+import sys
+import types
+from types import ModuleType
+from typing import Any
+from typing import TypeVar
+import uuid
+import warnings
+
+from _pytest.compat import assert_never
+from _pytest.outcomes import skip
+from _pytest.warning_types import PytestWarning
+
+
+if sys.version_info < (3, 11):
+ from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader
+else:
+ from importlib.machinery import NamespaceLoader
+
+LOCK_TIMEOUT = 60 * 60 * 24 * 3
+
+_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath)
+
+# The following function, variables and comments were
+# copied from cpython 3.9 Lib/pathlib.py file.
+
+# EBADF - guard against macOS `stat` throwing EBADF
+_IGNORED_ERRORS = (ENOENT, ENOTDIR, EBADF, ELOOP)
+
+_IGNORED_WINERRORS = (
+ 21, # ERROR_NOT_READY - drive exists but is not accessible
+ 1921, # ERROR_CANT_RESOLVE_FILENAME - fix for broken symlink pointing to itself
+)
+
+
+def _ignore_error(exception: Exception) -> bool:
+ return (
+ getattr(exception, "errno", None) in _IGNORED_ERRORS
+ or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
+ )
+
+
+def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
+ return path.joinpath(".lock")
+
+
+def on_rm_rf_error(
+ func: Callable[..., Any] | None,
+ path: str,
+ excinfo: BaseException
+ | tuple[type[BaseException], BaseException, types.TracebackType | None],
+ *,
+ start_path: Path,
+) -> bool:
+ """Handle known read-only errors during rmtree.
+
+ The returned value is used only by our own tests.
+ """
+ if isinstance(excinfo, BaseException):
+ exc = excinfo
+ else:
+ exc = excinfo[1]
+
+ # Another process removed the file in the middle of the "rm_rf" (xdist for example).
+ # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
+ if isinstance(exc, FileNotFoundError):
+ return False
+
+ if not isinstance(exc, PermissionError):
+ warnings.warn(
+ PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
+ )
+ return False
+
+ if func not in (os.rmdir, os.remove, os.unlink):
+ if func not in (os.open,):
+ warnings.warn(
+ PytestWarning(
+ f"(rm_rf) unknown function {func} when removing {path}:\n{type(exc)}: {exc}"
+ )
+ )
+ return False
+
+ # Chmod + retry.
+ import stat
+
+ def chmod_rw(p: str) -> None:
+ mode = os.stat(p).st_mode
+ os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
+
+ # For files, we need to recursively go upwards in the directories to
+ # ensure they all are also writable.
+ p = Path(path)
+ if p.is_file():
+ for parent in p.parents:
+ chmod_rw(str(parent))
+ # Stop when we reach the original path passed to rm_rf.
+ if parent == start_path:
+ break
+ chmod_rw(str(path))
+
+ func(path)
+ return True
+
+
+def ensure_extended_length_path(path: Path) -> Path:
+ """Get the extended-length version of a path (Windows).
+
+ On Windows, by default, the maximum length of a path (MAX_PATH) is 260
+ characters, and operations on paths longer than that fail. But it is possible
+ to overcome this by converting the path to "extended-length" form before
+ performing the operation:
+ https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#maximum-path-length-limitation
+
+ On Windows, this function returns the extended-length absolute version of path.
+ On other platforms it returns path unchanged.
+ """
+ if sys.platform.startswith("win32"):
+ path = path.resolve()
+ path = Path(get_extended_length_path_str(str(path)))
+ return path
+
+
+def get_extended_length_path_str(path: str) -> str:
+ """Convert a path to a Windows extended length path."""
+ long_path_prefix = "\\\\?\\"
+ unc_long_path_prefix = "\\\\?\\UNC\\"
+ if path.startswith((long_path_prefix, unc_long_path_prefix)):
+ return path
+ # UNC
+ if path.startswith("\\\\"):
+ return unc_long_path_prefix + path[2:]
+ return long_path_prefix + path
+
+
+def rm_rf(path: Path) -> None:
+ """Remove the path contents recursively, even if some elements
+ are read-only."""
+ path = ensure_extended_length_path(path)
+ onerror = partial(on_rm_rf_error, start_path=path)
+ if sys.version_info >= (3, 12):
+ shutil.rmtree(str(path), onexc=onerror)
+ else:
+ shutil.rmtree(str(path), onerror=onerror)
+
+
+def find_prefixed(root: Path, prefix: str) -> Iterator[os.DirEntry[str]]:
+ """Find all elements in root that begin with the prefix, case-insensitive."""
+ l_prefix = prefix.lower()
+ for x in os.scandir(root):
+ if x.name.lower().startswith(l_prefix):
+ yield x
+
+
+def extract_suffixes(iter: Iterable[os.DirEntry[str]], prefix: str) -> Iterator[str]:
+ """Return the parts of the paths following the prefix.
+
+ :param iter: Iterator over path names.
+ :param prefix: Expected prefix of the path names.
+ """
+ p_len = len(prefix)
+ for entry in iter:
+ yield entry.name[p_len:]
+
+
+def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
+ """Combine find_prefixes and extract_suffixes."""
+ return extract_suffixes(find_prefixed(root, prefix), prefix)
+
+
+def parse_num(maybe_num: str) -> int:
+ """Parse number path suffixes, returns -1 on error."""
+ try:
+ return int(maybe_num)
+ except ValueError:
+ return -1
+
+
+def _force_symlink(root: Path, target: str | PurePath, link_to: str | Path) -> None:
+ """Helper to create the current symlink.
+
+ It's full of race conditions that are reasonably OK to ignore
+ for the context of best effort linking to the latest test run.
+
+ The presumption being that in case of much parallelism
+ the inaccuracy is going to be acceptable.
+ """
+ current_symlink = root.joinpath(target)
+ try:
+ current_symlink.unlink()
+ except OSError:
+ pass
+ try:
+ current_symlink.symlink_to(link_to)
+ except Exception:
+ pass
+
+
+def make_numbered_dir(root: Path, prefix: str, mode: int = 0o700) -> Path:
+ """Create a directory with an increased number as suffix for the given prefix."""
+ for i in range(10):
+ # try up to 10 times to create the directory
+ max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
+ new_number = max_existing + 1
+ new_path = root.joinpath(f"{prefix}{new_number}")
+ try:
+ new_path.mkdir(mode=mode)
+ except Exception:
+ pass
+ else:
+ _force_symlink(root, prefix + "current", new_path)
+ return new_path
+ else:
+ raise OSError(
+ "could not create numbered dir with prefix "
+ f"{prefix} in {root} after 10 tries"
+ )
+
+
+def create_cleanup_lock(p: Path) -> Path:
+ """Create a lock to prevent premature directory cleanup."""
+ lock_path = get_lock_path(p)
+ try:
+ fd = os.open(str(lock_path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644)
+ except FileExistsError as e:
+ raise OSError(f"cannot create lockfile in {p}") from e
+ else:
+ pid = os.getpid()
+ spid = str(pid).encode()
+ os.write(fd, spid)
+ os.close(fd)
+ if not lock_path.is_file():
+ raise OSError("lock path got renamed after successful creation")
+ return lock_path
+
+
+def register_cleanup_lock_removal(lock_path: Path, register: Any) -> Any:
+ """Register a cleanup function for removing a lock."""
+ pid = os.getpid()
+
+ def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None:
+ current_pid = os.getpid()
+ if current_pid != original_pid:
+ # fork
+ return
+ try:
+ lock_path.unlink()
+ except OSError:
+ pass
+
+ return register(cleanup_on_exit)
+
+
+def maybe_delete_a_numbered_dir(path: Path) -> None:
+ """Remove a numbered directory if its lock can be obtained and it does
+ not seem to be in use."""
+ path = ensure_extended_length_path(path)
+ lock_path = None
+ try:
+ lock_path = create_cleanup_lock(path)
+ parent = path.parent
+
+ garbage = parent.joinpath(f"garbage-{uuid.uuid4()}")
+ path.rename(garbage)
+ rm_rf(garbage)
+ except OSError:
+ # known races:
+ # * other process did a cleanup at the same time
+ # * deletable directory was found
+ # * process cwd (Windows)
+ return
+ finally:
+ # If we created the lock, ensure we remove it even if we failed
+ # to properly remove the numbered dir.
+ if lock_path is not None:
+ try:
+ lock_path.unlink()
+ except OSError:
+ pass
+
+
+def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool:
+ """Check if `path` is deletable based on whether the lock file is expired."""
+ if path.is_symlink():
+ return False
+ lock = get_lock_path(path)
+ try:
+ if not lock.is_file():
+ return True
+ except OSError:
+ # we might not have access to the lock file at all, in this case assume
+ # we don't have access to the entire directory (#7491).
+ return False
+ try:
+ lock_time = lock.stat().st_mtime
+ except Exception:
+ return False
+ else:
+ if lock_time < consider_lock_dead_if_created_before:
+ # We want to ignore any errors while trying to remove the lock such as:
+ # - PermissionDenied, like the file permissions have changed since the lock creation;
+ # - FileNotFoundError, in case another pytest process got here first;
+ # and any other cause of failure.
+ with contextlib.suppress(OSError):
+ lock.unlink()
+ return True
+ return False
+
+
+def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None:
+ """Try to cleanup a directory if we can ensure it's deletable."""
+ if ensure_deletable(path, consider_lock_dead_if_created_before):
+ maybe_delete_a_numbered_dir(path)
+
+
+def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
+ """List candidates for numbered directories to be removed - follows py.path."""
+ max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
+ max_delete = max_existing - keep
+ entries = find_prefixed(root, prefix)
+ entries, entries2 = itertools.tee(entries)
+ numbers = map(parse_num, extract_suffixes(entries2, prefix))
+ for entry, number in zip(entries, numbers, strict=True):
+ if number <= max_delete:
+ yield Path(entry)
+
+
+def cleanup_dead_symlinks(root: Path) -> None:
+ for left_dir in root.iterdir():
+ if left_dir.is_symlink():
+ if not left_dir.resolve().exists():
+ left_dir.unlink()
+
+
+def cleanup_numbered_dir(
+ root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
+) -> None:
+ """Cleanup for lock driven numbered directories."""
+ if not root.exists():
+ return
+ for path in cleanup_candidates(root, prefix, keep):
+ try_cleanup(path, consider_lock_dead_if_created_before)
+ for path in root.glob("garbage-*"):
+ try_cleanup(path, consider_lock_dead_if_created_before)
+
+ cleanup_dead_symlinks(root)
+
+
+def make_numbered_dir_with_cleanup(
+ *,
+ root: Path,
+ prefix: str,
+ mode: int,
+ keep: int,
+ lock_timeout: float,
+ register: Any,
+) -> Path:
+ """Create a numbered dir and register its cleanup.
+
+ Similar to make_numbered_dir, but also maintains a lock file indicating that
+ the directory is currently in use, and registers the cleanup of the lock and
+ of stale numbered directories.
+
+ :param keep:
+ The number of sessions to retain the directory.
+ :param lock_timeout:
+ In case of a crash, the lock remains "stuck". The timeout is a time
+ limit after which the lock is considered stale and can be removed.
+ :param register:
+ Called as register(cleanup_func, params...). Should schedule to call
+ passed cleanup functions on session finish.
+ """
+ e = None
+ for i in range(10):
+ try:
+ p = make_numbered_dir(root, prefix, mode)
+ # Only lock the current dir when keep is not 0
+ if keep != 0:
+ lock_path = create_cleanup_lock(p)
+ register_cleanup_lock_removal(lock_path, register)
+ except Exception as exc:
+ e = exc
+ else:
+ consider_lock_dead_if_created_before = p.stat().st_mtime - lock_timeout
+ # Register a cleanup for program exit
+ register(
+ cleanup_numbered_dir,
+ root,
+ prefix,
+ keep,
+ consider_lock_dead_if_created_before,
+ )
+ return p
+ assert e is not None
+ raise e
+
+
+def resolve_from_str(input: str, rootpath: Path) -> Path:
+ input = expanduser(input)
+ input = expandvars(input)
+ if isabs(input):
+ return Path(input)
+ else:
+ return rootpath.joinpath(input)
+
+
+def fnmatch_ex(pattern: str, path: str | os.PathLike[str]) -> bool:
+ """A port of FNMatcher from py.path.common which works with PurePath() instances.
+
+ The difference between this algorithm and PurePath.match() is that the
+ latter matches "**" glob expressions for each part of the path, while
+ this algorithm uses the whole path instead.
+
+ For example:
+ "tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py"
+ with this algorithm, but not with PurePath.match().
+
+ This algorithm was ported to keep backward-compatibility with existing
+ settings which assume paths match according this logic.
+
+ References:
+ * https://bugs.python.org/issue29249
+ * https://bugs.python.org/issue34731
+ """
+ path = PurePath(path)
+ iswin32 = sys.platform.startswith("win")
+
+ if iswin32 and sep not in pattern and posix_sep in pattern:
+ # Running on Windows, the pattern has no Windows path separators,
+ # and the pattern has one or more Posix path separators. Replace
+ # the Posix path separators with the Windows path separator.
+ pattern = pattern.replace(posix_sep, sep)
+
+ if sep not in pattern:
+ name = path.name
+ else:
+ name = str(path)
+ if path.is_absolute() and not os.path.isabs(pattern):
+ pattern = f"*{os.sep}{pattern}"
+ return fnmatch.fnmatch(name, pattern)
+
+
+def parts(s: str) -> set[str]:
+ parts = s.split(sep)
+ return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
+
+
+def symlink_or_skip(
+ src: os.PathLike[str] | str,
+ dst: os.PathLike[str] | str,
+ **kwargs: Any,
+) -> None:
+ """Make a symlink, or skip the test in case symlinks are not supported."""
+ try:
+ os.symlink(src, dst, **kwargs)
+ except OSError as e:
+ skip(f"symlinks not supported: {e}")
+
+
+class ImportMode(Enum):
+ """Possible values for `mode` parameter of `import_path`."""
+
+ prepend = "prepend"
+ append = "append"
+ importlib = "importlib"
+
+
+class ImportPathMismatchError(ImportError):
+ """Raised on import_path() if there is a mismatch of __file__'s.
+
+ This can happen when `import_path` is called multiple times with different filenames that has
+ the same basename but reside in packages
+ (for example "/tests1/test_foo.py" and "/tests2/test_foo.py").
+ """
+
+
+def import_path(
+ path: str | os.PathLike[str],
+ *,
+ mode: str | ImportMode = ImportMode.prepend,
+ root: Path,
+ consider_namespace_packages: bool,
+) -> ModuleType:
+ """
+ Import and return a module from the given path, which can be a file (a module) or
+ a directory (a package).
+
+ :param path:
+ Path to the file to import.
+
+ :param mode:
+ Controls the underlying import mechanism that will be used:
+
+ * ImportMode.prepend: the directory containing the module (or package, taking
+ `__init__.py` files into account) will be put at the *start* of `sys.path` before
+ being imported with `importlib.import_module`.
+
+ * ImportMode.append: same as `prepend`, but the directory will be appended
+ to the end of `sys.path`, if not already in `sys.path`.
+
+ * ImportMode.importlib: uses more fine control mechanisms provided by `importlib`
+ to import the module, which avoids having to muck with `sys.path` at all. It effectively
+ allows having same-named test modules in different places.
+
+ :param root:
+ Used as an anchor when mode == ImportMode.importlib to obtain
+ a unique name for the module being imported so it can safely be stored
+ into ``sys.modules``.
+
+ :param consider_namespace_packages:
+ If True, consider namespace packages when resolving module names.
+
+ :raises ImportPathMismatchError:
+ If after importing the given `path` and the module `__file__`
+ are different. Only raised in `prepend` and `append` modes.
+ """
+ path = Path(path)
+ mode = ImportMode(mode)
+
+ if not path.exists():
+ raise ImportError(path)
+
+ if mode is ImportMode.importlib:
+ # Try to import this module using the standard import mechanisms, but
+ # without touching sys.path.
+ try:
+ _, module_name = resolve_pkg_root_and_module_name(
+ path, consider_namespace_packages=consider_namespace_packages
+ )
+ except CouldNotResolvePathError:
+ pass
+ else:
+ # If the given module name is already in sys.modules, do not import it again.
+ with contextlib.suppress(KeyError):
+ return sys.modules[module_name]
+
+ mod = _import_module_using_spec(module_name, path, insert_modules=False)
+ if mod is not None:
+ return mod
+
+ # Could not import the module with the current sys.path, so we fall back
+ # to importing the file as a single module, not being a part of a package.
+ module_name = module_name_from_path(path, root)
+ with contextlib.suppress(KeyError):
+ return sys.modules[module_name]
+
+ mod = _import_module_using_spec(module_name, path, insert_modules=True)
+ if mod is None:
+ raise ImportError(f"Can't find module {module_name} at location {path}")
+ return mod
+
+ try:
+ pkg_root, module_name = resolve_pkg_root_and_module_name(
+ path, consider_namespace_packages=consider_namespace_packages
+ )
+ except CouldNotResolvePathError:
+ pkg_root, module_name = path.parent, path.stem
+
+ # Change sys.path permanently: restoring it at the end of this function would cause surprising
+ # problems because of delayed imports: for example, a conftest.py file imported by this function
+ # might have local imports, which would fail at runtime if we restored sys.path.
+ if mode is ImportMode.append:
+ if str(pkg_root) not in sys.path:
+ sys.path.append(str(pkg_root))
+ elif mode is ImportMode.prepend:
+ if str(pkg_root) != sys.path[0]:
+ sys.path.insert(0, str(pkg_root))
+ else:
+ assert_never(mode)
+
+ importlib.import_module(module_name)
+
+ mod = sys.modules[module_name]
+ if path.name == "__init__.py":
+ return mod
+
+ ignore = os.environ.get("PY_IGNORE_IMPORTMISMATCH", "")
+ if ignore != "1":
+ module_file = mod.__file__
+ if module_file is None:
+ raise ImportPathMismatchError(module_name, module_file, path)
+
+ if module_file.endswith((".pyc", ".pyo")):
+ module_file = module_file[:-1]
+ if module_file.endswith(os.sep + "__init__.py"):
+ module_file = module_file[: -(len(os.sep + "__init__.py"))]
+
+ try:
+ is_same = _is_same(str(path), module_file)
+ except FileNotFoundError:
+ is_same = False
+
+ if not is_same:
+ raise ImportPathMismatchError(module_name, module_file, path)
+
+ return mod
+
+
+def _import_module_using_spec(
+ module_name: str, module_path: Path, *, insert_modules: bool
+) -> ModuleType | None:
+ """
+ Tries to import a module by its canonical name, path, and its parent location.
+
+ :param module_name:
+ The expected module name, will become the key of `sys.modules`.
+
+ :param module_path:
+ The file path of the module, for example `/foo/bar/test_demo.py`.
+ If module is a package, pass the path to the `__init__.py` of the package.
+ If module is a namespace package, pass directory path.
+
+ :param insert_modules:
+ If True, will call `insert_missing_modules` to create empty intermediate modules
+ with made-up module names (when importing test files not reachable from `sys.path`).
+
+ Example 1 of parent_module_*:
+
+ module_name: "a.b.c.demo"
+ module_path: Path("a/b/c/demo.py")
+ if "a.b.c" is package ("a/b/c/__init__.py" exists), then
+ parent_module_name: "a.b.c"
+ parent_module_path: Path("a/b/c/__init__.py")
+ else:
+ parent_module_name: "a.b.c"
+ parent_module_path: Path("a/b/c")
+
+ Example 2 of parent_module_*:
+
+ module_name: "a.b.c"
+ module_path: Path("a/b/c/__init__.py")
+ if "a.b" is package ("a/b/__init__.py" exists), then
+ parent_module_name: "a.b"
+ parent_module_path: Path("a/b/__init__.py")
+ else:
+ parent_module_name: "a.b"
+ parent_module_path: Path("a/b/")
+ """
+ # Attempt to import the parent module, seems is our responsibility:
+ # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
+ parent_module_name, _, name = module_name.rpartition(".")
+ parent_module: ModuleType | None = None
+ if parent_module_name:
+ parent_module = sys.modules.get(parent_module_name)
+ # If the parent_module lacks the `__path__` attribute, AttributeError when finding a submodule's spec,
+ # requiring re-import according to the path.
+ need_reimport = not hasattr(parent_module, "__path__")
+ if parent_module is None or need_reimport:
+ # Get parent_location based on location, get parent_path based on path.
+ if module_path.name == "__init__.py":
+ # If the current module is in a package,
+ # need to leave the package first and then enter the parent module.
+ parent_module_path = module_path.parent.parent
+ else:
+ parent_module_path = module_path.parent
+
+ if (parent_module_path / "__init__.py").is_file():
+ # If the parent module is a package, loading by __init__.py file.
+ parent_module_path = parent_module_path / "__init__.py"
+
+ parent_module = _import_module_using_spec(
+ parent_module_name,
+ parent_module_path,
+ insert_modules=insert_modules,
+ )
+
+ # Checking with sys.meta_path first in case one of its hooks can import this module,
+ # such as our own assertion-rewrite hook.
+ find_spec_path = [str(module_path.parent)]
+ for meta_importer in sys.meta_path:
+ spec = meta_importer.find_spec(module_name, find_spec_path)
+
+ if spec_matches_module_path(spec, module_path):
+ break
+ else:
+ loader = None
+ if module_path.is_dir():
+ # The `spec_from_file_location` matches a loader based on the file extension by default.
+ # For a namespace package, need to manually specify a loader.
+ loader = NamespaceLoader(name, module_path, PathFinder()) # type: ignore[arg-type]
+
+ spec = importlib.util.spec_from_file_location(
+ module_name, str(module_path), loader=loader
+ )
+
+ if spec_matches_module_path(spec, module_path):
+ assert spec is not None
+ # Find spec and import this module.
+ mod = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = mod
+ spec.loader.exec_module(mod) # type: ignore[union-attr]
+
+ # Set this module as an attribute of the parent module (#12194).
+ if parent_module is not None:
+ setattr(parent_module, name, mod)
+
+ if insert_modules:
+ insert_missing_modules(sys.modules, module_name)
+ return mod
+
+ return None
+
+
+def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool:
+ """Return true if the given ModuleSpec can be used to import the given module path."""
+ if module_spec is None:
+ return False
+
+ if module_spec.origin:
+ return Path(module_spec.origin) == module_path
+
+ # Compare the path with the `module_spec.submodule_Search_Locations` in case
+ # the module is part of a namespace package.
+ # https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations
+ if module_spec.submodule_search_locations: # can be None.
+ for path in module_spec.submodule_search_locations:
+ if Path(path) == module_path:
+ return True
+
+ return False
+
+
+# Implement a special _is_same function on Windows which returns True if the two filenames
+# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
+if sys.platform.startswith("win"):
+
+ def _is_same(f1: str, f2: str) -> bool:
+ return Path(f1) == Path(f2) or os.path.samefile(f1, f2)
+
+else:
+
+ def _is_same(f1: str, f2: str) -> bool:
+ return os.path.samefile(f1, f2)
+
+
+def module_name_from_path(path: Path, root: Path) -> str:
+ """
+ Return a dotted module name based on the given path, anchored on root.
+
+ For example: path="projects/src/tests/test_foo.py" and root="/projects", the
+ resulting module name will be "src.tests.test_foo".
+ """
+ path = path.with_suffix("")
+ try:
+ relative_path = path.relative_to(root)
+ except ValueError:
+ # If we can't get a relative path to root, use the full path, except
+ # for the first part ("d:\\" or "/" depending on the platform, for example).
+ path_parts = path.parts[1:]
+ else:
+ # Use the parts for the relative path to the root path.
+ path_parts = relative_path.parts
+
+ # Module name for packages do not contain the __init__ file, unless
+ # the `__init__.py` file is at the root.
+ if len(path_parts) >= 2 and path_parts[-1] == "__init__":
+ path_parts = path_parts[:-1]
+
+ # Module names cannot contain ".", normalize them to "_". This prevents
+ # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules.
+ # Also, important to replace "." at the start of paths, as those are considered relative imports.
+ path_parts = tuple(x.replace(".", "_") for x in path_parts)
+
+ return ".".join(path_parts)
+
+
+def insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> None:
+ """
+ Used by ``import_path`` to create intermediate modules when using mode=importlib.
+
+ When we want to import a module as "src.tests.test_foo" for example, we need
+ to create empty modules "src" and "src.tests" after inserting "src.tests.test_foo",
+ otherwise "src.tests.test_foo" is not importable by ``__import__``.
+ """
+ module_parts = module_name.split(".")
+ while module_name:
+ parent_module_name, _, child_name = module_name.rpartition(".")
+ if parent_module_name:
+ parent_module = modules.get(parent_module_name)
+ if parent_module is None:
+ try:
+ # If sys.meta_path is empty, calling import_module will issue
+ # a warning and raise ModuleNotFoundError. To avoid the
+ # warning, we check sys.meta_path explicitly and raise the error
+ # ourselves to fall back to creating a dummy module.
+ if not sys.meta_path:
+ raise ModuleNotFoundError
+ parent_module = importlib.import_module(parent_module_name)
+ except ModuleNotFoundError:
+ parent_module = ModuleType(
+ module_name,
+ doc="Empty module created by pytest's importmode=importlib.",
+ )
+ modules[parent_module_name] = parent_module
+
+ # Add child attribute to the parent that can reference the child
+ # modules.
+ if not hasattr(parent_module, child_name):
+ setattr(parent_module, child_name, modules[module_name])
+
+ module_parts.pop(-1)
+ module_name = ".".join(module_parts)
+
+
+def resolve_package_path(path: Path) -> Path | None:
+ """Return the Python package path by looking for the last
+ directory upwards which still contains an __init__.py.
+
+ Returns None if it cannot be determined.
+ """
+ result = None
+ for parent in itertools.chain((path,), path.parents):
+ if parent.is_dir():
+ if not (parent / "__init__.py").is_file():
+ break
+ if not parent.name.isidentifier():
+ break
+ result = parent
+ return result
+
+
+def resolve_pkg_root_and_module_name(
+ path: Path, *, consider_namespace_packages: bool = False
+) -> tuple[Path, str]:
+ """
+ Return the path to the directory of the root package that contains the
+ given Python file, and its module name:
+
+ src/
+ app/
+ __init__.py
+ core/
+ __init__.py
+ models.py
+
+ Passing the full path to `models.py` will yield Path("src") and "app.core.models".
+
+ If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
+ for namespace packages:
+
+ https://packaging.python.org/en/latest/guides/packaging-namespace-packages
+
+ Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
+ """
+ pkg_root: Path | None = None
+ pkg_path = resolve_package_path(path)
+ if pkg_path is not None:
+ pkg_root = pkg_path.parent
+ if consider_namespace_packages:
+ start = pkg_root if pkg_root is not None else path.parent
+ for candidate in (start, *start.parents):
+ module_name = compute_module_name(candidate, path)
+ if module_name and is_importable(module_name, path):
+ # Point the pkg_root to the root of the namespace package.
+ pkg_root = candidate
+ break
+
+ if pkg_root is not None:
+ module_name = compute_module_name(pkg_root, path)
+ if module_name:
+ return pkg_root, module_name
+
+ raise CouldNotResolvePathError(f"Could not resolve for {path}")
+
+
+def is_importable(module_name: str, module_path: Path) -> bool:
+ """
+ Return if the given module path could be imported normally by Python, akin to the user
+ entering the REPL and importing the corresponding module name directly, and corresponds
+ to the module_path specified.
+
+ :param module_name:
+ Full module name that we want to check if is importable.
+ For example, "app.models".
+
+ :param module_path:
+ Full path to the python module/package we want to check if is importable.
+ For example, "/projects/src/app/models.py".
+ """
+ try:
+ # Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through
+ # sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``).
+ # Using importlib.util.find_spec() is different, it gives the same results as trying to import
+ # the module normally in the REPL.
+ spec = importlib.util.find_spec(module_name)
+ except (ImportError, ValueError, ImportWarning):
+ return False
+ else:
+ return spec_matches_module_path(spec, module_path)
+
+
+def compute_module_name(root: Path, module_path: Path) -> str | None:
+ """Compute a module name based on a path and a root anchor."""
+ try:
+ path_without_suffix = module_path.with_suffix("")
+ except ValueError:
+ # Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter).
+ return None
+
+ try:
+ relative = path_without_suffix.relative_to(root)
+ except ValueError: # pragma: no cover
+ return None
+ names = list(relative.parts)
+ if not names:
+ return None
+ if names[-1] == "__init__":
+ names.pop()
+ return ".".join(names)
+
+
+class CouldNotResolvePathError(Exception):
+ """Custom exception raised by resolve_pkg_root_and_module_name."""
+
+
+def scandir(
+ path: str | os.PathLike[str],
+ sort_key: Callable[[os.DirEntry[str]], object] = lambda entry: entry.name,
+) -> list[os.DirEntry[str]]:
+ """Scan a directory recursively, in breadth-first order.
+
+ The returned entries are sorted according to the given key.
+ The default is to sort by name.
+ If the directory does not exist, return an empty list.
+ """
+ entries = []
+ # Attempt to create a scandir iterator for the given path.
+ try:
+ scandir_iter = os.scandir(path)
+ except FileNotFoundError:
+ # If the directory does not exist, return an empty list.
+ return []
+ # Use the scandir iterator in a context manager to ensure it is properly closed.
+ with scandir_iter as s:
+ for entry in s:
+ try:
+ entry.is_file()
+ except OSError as err:
+ if _ignore_error(err):
+ continue
+ # Reraise non-ignorable errors to avoid hiding issues.
+ raise
+ entries.append(entry)
+ entries.sort(key=sort_key) # type: ignore[arg-type]
+ return entries
+
+
+def visit(
+ path: str | os.PathLike[str], recurse: Callable[[os.DirEntry[str]], bool]
+) -> Iterator[os.DirEntry[str]]:
+ """Walk a directory recursively, in breadth-first order.
+
+ The `recurse` predicate determines whether a directory is recursed.
+
+ Entries at each directory level are sorted.
+ """
+ entries = scandir(path)
+ yield from entries
+ for entry in entries:
+ if entry.is_dir() and recurse(entry):
+ yield from visit(entry.path, recurse)
+
+
+def absolutepath(path: str | os.PathLike[str]) -> Path:
+ """Convert a path to an absolute path using os.path.abspath.
+
+ Prefer this over Path.resolve() (see #6523).
+ Prefer this over Path.absolute() (not public, doesn't normalize).
+ """
+ return Path(os.path.abspath(path))
+
+
+def commonpath(path1: Path, path2: Path) -> Path | None:
+ """Return the common part shared with the other path, or None if there is
+ no common part.
+
+ If one path is relative and one is absolute, returns None.
+ """
+ try:
+ return Path(os.path.commonpath((str(path1), str(path2))))
+ except ValueError:
+ return None
+
+
+def bestrelpath(directory: Path, dest: Path) -> str:
+ """Return a string which is a relative path from directory to dest such
+ that directory/bestrelpath == dest.
+
+ The paths must be either both absolute or both relative.
+
+ If no such path can be determined, returns dest.
+ """
+ assert isinstance(directory, Path)
+ assert isinstance(dest, Path)
+ if dest == directory:
+ return os.curdir
+ # Find the longest common directory.
+ base = commonpath(directory, dest)
+ # Can be the case on Windows for two absolute paths on different drives.
+ # Can be the case for two relative paths without common prefix.
+ # Can be the case for a relative path and an absolute path.
+ if not base:
+ return str(dest)
+ reldirectory = directory.relative_to(base)
+ reldest = dest.relative_to(base)
+ return os.path.join(
+ # Back from directory to base.
+ *([os.pardir] * len(reldirectory.parts)),
+ # Forward from base to dest.
+ *reldest.parts,
+ )
+
+
+def safe_exists(p: Path) -> bool:
+ """Like Path.exists(), but account for input arguments that might be too long (#11394)."""
+ try:
+ return p.exists()
+ except (ValueError, OSError):
+ # ValueError: stat: path too long for Windows
+ # OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
+ return False
+
+
+def samefile_nofollow(p1: Path, p2: Path) -> bool:
+ """Test whether two paths reference the same actual file or directory.
+
+ Unlike Path.samefile(), does not resolve symlinks.
+ """
+ return os.path.samestat(p1.lstat(), p2.lstat())
diff --git a/venv/lib/python3.11/site-packages/_pytest/py.typed b/venv/lib/python3.11/site-packages/_pytest/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/_pytest/pytester.py b/venv/lib/python3.11/site-packages/_pytest/pytester.py
new file mode 100644
index 000000000..b69b58732
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/pytester.py
@@ -0,0 +1,1803 @@
+# mypy: allow-untyped-defs
+"""(Disabled by default) support for testing pytest and pytest plugins.
+
+PYTEST_DONT_REWRITE
+"""
+
+from __future__ import annotations
+
+import collections.abc
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Mapping
+from collections.abc import Sequence
+import contextlib
+from fnmatch import fnmatch
+import gc
+import importlib
+from io import StringIO
+import locale
+import os
+from pathlib import Path
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import traceback
+from typing import Any
+from typing import Final
+from typing import final
+from typing import IO
+from typing import Literal
+from typing import overload
+from typing import TextIO
+from typing import TYPE_CHECKING
+from weakref import WeakKeyDictionary
+
+from iniconfig import IniConfig
+from iniconfig import SectionWrapper
+
+from _pytest import timing
+from _pytest._code import Source
+from _pytest.capture import _get_multicapture
+from _pytest.compat import NOTSET
+from _pytest.compat import NotSetType
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config import main
+from _pytest.config import PytestPluginManager
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.main import Session
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import fail
+from _pytest.outcomes import importorskip
+from _pytest.outcomes import skip
+from _pytest.pathlib import bestrelpath
+from _pytest.pathlib import make_numbered_dir
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+from _pytest.tmpdir import TempPathFactory
+from _pytest.warning_types import PytestFDWarning
+
+
+if TYPE_CHECKING:
+ import pexpect
+
+
+pytest_plugins = ["pytester_assertions"]
+
+
+IGNORE_PAM = [ # filenames added when obtaining details about the current user
+ "/var/lib/sss/mc/passwd"
+]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addoption(
+ "--lsof",
+ action="store_true",
+ dest="lsof",
+ default=False,
+ help="Run FD checks if lsof is available",
+ )
+
+ parser.addoption(
+ "--runpytest",
+ default="inprocess",
+ dest="runpytest",
+ choices=("inprocess", "subprocess"),
+ help=(
+ "Run pytest sub runs in tests using an 'inprocess' "
+ "or 'subprocess' (python -m main) method"
+ ),
+ )
+
+ parser.addini(
+ "pytester_example_dir", help="Directory to take the pytester example files from"
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ if config.getvalue("lsof"):
+ checker = LsofFdLeakChecker()
+ if checker.matching_platform():
+ config.pluginmanager.register(checker)
+
+ config.addinivalue_line(
+ "markers",
+ "pytester_example_path(*path_segments): join the given path "
+ "segments to `pytester_example_dir` for this test.",
+ )
+
+
+class LsofFdLeakChecker:
+ def get_open_files(self) -> list[tuple[str, str]]:
+ if sys.version_info >= (3, 11):
+ # New in Python 3.11, ignores utf-8 mode
+ encoding = locale.getencoding()
+ else:
+ encoding = locale.getpreferredencoding(False)
+ out = subprocess.run(
+ ("lsof", "-Ffn0", "-p", str(os.getpid())),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ text=True,
+ encoding=encoding,
+ ).stdout
+
+ def isopen(line: str) -> bool:
+ return line.startswith("f") and (
+ "deleted" not in line
+ and "mem" not in line
+ and "txt" not in line
+ and "cwd" not in line
+ )
+
+ open_files = []
+
+ for line in out.split("\n"):
+ if isopen(line):
+ fields = line.split("\0")
+ fd = fields[0][1:]
+ filename = fields[1][1:]
+ if filename in IGNORE_PAM:
+ continue
+ if filename.startswith("/"):
+ open_files.append((fd, filename))
+
+ return open_files
+
+ def matching_platform(self) -> bool:
+ try:
+ subprocess.run(("lsof", "-v"), check=True)
+ except (OSError, subprocess.CalledProcessError):
+ return False
+ else:
+ return True
+
+ @hookimpl(wrapper=True, tryfirst=True)
+ def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]:
+ lines1 = self.get_open_files()
+ try:
+ return (yield)
+ finally:
+ if hasattr(sys, "pypy_version_info"):
+ gc.collect()
+ lines2 = self.get_open_files()
+
+ new_fds = {t[0] for t in lines2} - {t[0] for t in lines1}
+ leaked_files = [t for t in lines2 if t[0] in new_fds]
+ if leaked_files:
+ error = [
+ f"***** {len(leaked_files)} FD leakage detected",
+ *(str(f) for f in leaked_files),
+ "*** Before:",
+ *(str(f) for f in lines1),
+ "*** After:",
+ *(str(f) for f in lines2),
+ f"***** {len(leaked_files)} FD leakage detected",
+ "*** function {}:{}: {} ".format(*item.location),
+ "See issue #2366",
+ ]
+ item.warn(PytestFDWarning("\n".join(error)))
+
+
+# used at least by pytest-xdist plugin
+
+
+@fixture
+def _pytest(request: FixtureRequest) -> PytestArg:
+ """Return a helper which offers a gethookrecorder(hook) method which
+ returns a HookRecorder instance which helps to make assertions about called
+ hooks."""
+ return PytestArg(request)
+
+
+class PytestArg:
+ def __init__(self, request: FixtureRequest) -> None:
+ self._request = request
+
+ def gethookrecorder(self, hook) -> HookRecorder:
+ hookrecorder = HookRecorder(hook._pm)
+ self._request.addfinalizer(hookrecorder.finish_recording)
+ return hookrecorder
+
+
+def get_public_names(values: Iterable[str]) -> list[str]:
+ """Only return names from iterator values without a leading underscore."""
+ return [x for x in values if x[0] != "_"]
+
+
+@final
+class RecordedHookCall:
+ """A recorded call to a hook.
+
+ The arguments to the hook call are set as attributes.
+ For example:
+
+ .. code-block:: python
+
+ calls = hook_recorder.getcalls("pytest_runtest_setup")
+ # Suppose pytest_runtest_setup was called once with `item=an_item`.
+ assert calls[0].item is an_item
+ """
+
+ def __init__(self, name: str, kwargs) -> None:
+ self.__dict__.update(kwargs)
+ self._name = name
+
+ def __repr__(self) -> str:
+ d = self.__dict__.copy()
+ del d["_name"]
+ return f""
+
+ if TYPE_CHECKING:
+ # The class has undetermined attributes, this tells mypy about it.
+ def __getattr__(self, key: str): ...
+
+
+@final
+class HookRecorder:
+ """Record all hooks called in a plugin manager.
+
+ Hook recorders are created by :class:`Pytester`.
+
+ This wraps all the hook calls in the plugin manager, recording each call
+ before propagating the normal calls.
+ """
+
+ def __init__(
+ self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False
+ ) -> None:
+ check_ispytest(_ispytest)
+
+ self._pluginmanager = pluginmanager
+ self.calls: list[RecordedHookCall] = []
+ self.ret: int | ExitCode | None = None
+
+ def before(hook_name: str, hook_impls, kwargs) -> None:
+ self.calls.append(RecordedHookCall(hook_name, kwargs))
+
+ def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
+ pass
+
+ self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after)
+
+ def finish_recording(self) -> None:
+ self._undo_wrapping()
+
+ def getcalls(self, names: str | Iterable[str]) -> list[RecordedHookCall]:
+ """Get all recorded calls to hooks with the given names (or name)."""
+ if isinstance(names, str):
+ names = names.split()
+ return [call for call in self.calls if call._name in names]
+
+ def assert_contains(self, entries: Sequence[tuple[str, str]]) -> None:
+ __tracebackhide__ = True
+ i = 0
+ entries = list(entries)
+ # Since Python 3.13, f_locals is not a dict, but eval requires a dict.
+ backlocals = dict(sys._getframe(1).f_locals)
+ while entries:
+ name, check = entries.pop(0)
+ for ind, call in enumerate(self.calls[i:]):
+ if call._name == name:
+ print("NAMEMATCH", name, call)
+ if eval(check, backlocals, call.__dict__):
+ print("CHECKERMATCH", repr(check), "->", call)
+ else:
+ print("NOCHECKERMATCH", repr(check), "-", call)
+ continue
+ i += ind + 1
+ break
+ print("NONAMEMATCH", name, "with", call)
+ else:
+ fail(f"could not find {name!r} check {check!r}")
+
+ def popcall(self, name: str) -> RecordedHookCall:
+ __tracebackhide__ = True
+ for i, call in enumerate(self.calls):
+ if call._name == name:
+ del self.calls[i]
+ return call
+ lines = [f"could not find call {name!r}, in:"]
+ lines.extend([f" {x}" for x in self.calls])
+ fail("\n".join(lines))
+
+ def getcall(self, name: str) -> RecordedHookCall:
+ values = self.getcalls(name)
+ assert len(values) == 1, (name, values)
+ return values[0]
+
+ # functionality for test reports
+
+ @overload
+ def getreports(
+ self,
+ names: Literal["pytest_collectreport"],
+ ) -> Sequence[CollectReport]: ...
+
+ @overload
+ def getreports(
+ self,
+ names: Literal["pytest_runtest_logreport"],
+ ) -> Sequence[TestReport]: ...
+
+ @overload
+ def getreports(
+ self,
+ names: str | Iterable[str] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[CollectReport | TestReport]: ...
+
+ def getreports(
+ self,
+ names: str | Iterable[str] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[CollectReport | TestReport]:
+ return [x.report for x in self.getcalls(names)]
+
+ def matchreport(
+ self,
+ inamepart: str = "",
+ names: str | Iterable[str] = (
+ "pytest_runtest_logreport",
+ "pytest_collectreport",
+ ),
+ when: str | None = None,
+ ) -> CollectReport | TestReport:
+ """Return a testreport whose dotted import path matches."""
+ values = []
+ for rep in self.getreports(names=names):
+ if not when and rep.when != "call" and rep.passed:
+ # setup/teardown passing reports - let's ignore those
+ continue
+ if when and rep.when != when:
+ continue
+ if not inamepart or inamepart in rep.nodeid.split("::"):
+ values.append(rep)
+ if not values:
+ raise ValueError(
+ f"could not find test report matching {inamepart!r}: "
+ "no test reports at all!"
+ )
+ if len(values) > 1:
+ raise ValueError(
+ f"found 2 or more testreports matching {inamepart!r}: {values}"
+ )
+ return values[0]
+
+ @overload
+ def getfailures(
+ self,
+ names: Literal["pytest_collectreport"],
+ ) -> Sequence[CollectReport]: ...
+
+ @overload
+ def getfailures(
+ self,
+ names: Literal["pytest_runtest_logreport"],
+ ) -> Sequence[TestReport]: ...
+
+ @overload
+ def getfailures(
+ self,
+ names: str | Iterable[str] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[CollectReport | TestReport]: ...
+
+ def getfailures(
+ self,
+ names: str | Iterable[str] = (
+ "pytest_collectreport",
+ "pytest_runtest_logreport",
+ ),
+ ) -> Sequence[CollectReport | TestReport]:
+ return [rep for rep in self.getreports(names) if rep.failed]
+
+ def getfailedcollections(self) -> Sequence[CollectReport]:
+ return self.getfailures("pytest_collectreport")
+
+ def listoutcomes(
+ self,
+ ) -> tuple[
+ Sequence[TestReport],
+ Sequence[CollectReport | TestReport],
+ Sequence[CollectReport | TestReport],
+ ]:
+ passed = []
+ skipped = []
+ failed = []
+ for rep in self.getreports(
+ ("pytest_collectreport", "pytest_runtest_logreport")
+ ):
+ if rep.passed:
+ if rep.when == "call":
+ assert isinstance(rep, TestReport)
+ passed.append(rep)
+ elif rep.skipped:
+ skipped.append(rep)
+ else:
+ assert rep.failed, f"Unexpected outcome: {rep!r}"
+ failed.append(rep)
+ return passed, skipped, failed
+
+ def countoutcomes(self) -> list[int]:
+ return [len(x) for x in self.listoutcomes()]
+
+ def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None:
+ __tracebackhide__ = True
+ from _pytest.pytester_assertions import assertoutcome
+
+ outcomes = self.listoutcomes()
+ assertoutcome(
+ outcomes,
+ passed=passed,
+ skipped=skipped,
+ failed=failed,
+ )
+
+ def clear(self) -> None:
+ self.calls[:] = []
+
+
+@fixture
+def linecomp() -> LineComp:
+ """A :class: `LineComp` instance for checking that an input linearly
+ contains a sequence of strings."""
+ return LineComp()
+
+
+@fixture(name="LineMatcher")
+def LineMatcher_fixture(request: FixtureRequest) -> type[LineMatcher]:
+ """A reference to the :class: `LineMatcher`.
+
+ This is instantiable with a list of lines (without their trailing newlines).
+ This is useful for testing large texts, such as the output of commands.
+ """
+ return LineMatcher
+
+
+@fixture
+def pytester(
+ request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch
+) -> Pytester:
+ """
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
+ against expected output, perfect for black-box testing of pytest plugins.
+
+ It attempts to isolate the test run from external factors as much as possible, modifying
+ the current working directory to ``path`` and environment variables during initialization.
+
+ It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
+ fixture but provides methods which aid in testing pytest itself.
+ """
+ return Pytester(request, tmp_path_factory, monkeypatch, _ispytest=True)
+
+
+@fixture
+def _sys_snapshot() -> Generator[None]:
+ snappaths = SysPathsSnapshot()
+ snapmods = SysModulesSnapshot()
+ yield
+ snapmods.restore()
+ snappaths.restore()
+
+
+@fixture
+def _config_for_test() -> Generator[Config]:
+ from _pytest.config import get_config
+
+ config = get_config()
+ yield config
+ config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles.
+
+
+# Regex to match the session duration string in the summary: "74.34s".
+rex_session_duration = re.compile(r"\d+\.\d\ds")
+# Regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped".
+rex_outcome = re.compile(r"(\d+) (\w+)")
+
+
+@final
+class RunResult:
+ """The result of running a command from :class:`~pytest.Pytester`."""
+
+ def __init__(
+ self,
+ ret: int | ExitCode,
+ outlines: list[str],
+ errlines: list[str],
+ duration: float,
+ ) -> None:
+ try:
+ self.ret: int | ExitCode = ExitCode(ret)
+ """The return value."""
+ except ValueError:
+ self.ret = ret
+ self.outlines = outlines
+ """List of lines captured from stdout."""
+ self.errlines = errlines
+ """List of lines captured from stderr."""
+ self.stdout = LineMatcher(outlines)
+ """:class:`~pytest.LineMatcher` of stdout.
+
+ Use e.g. :func:`str(stdout) ` to reconstruct stdout, or the commonly used
+ :func:`stdout.fnmatch_lines() ` method.
+ """
+ self.stderr = LineMatcher(errlines)
+ """:class:`~pytest.LineMatcher` of stderr."""
+ self.duration = duration
+ """Duration in seconds."""
+
+ def __repr__(self) -> str:
+ return (
+ f""
+ )
+
+ def parseoutcomes(self) -> dict[str, int]:
+ """Return a dictionary of outcome noun -> count from parsing the terminal
+ output that the test process produced.
+
+ The returned nouns will always be in plural form::
+
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
+
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
+ """
+ return self.parse_summary_nouns(self.outlines)
+
+ @classmethod
+ def parse_summary_nouns(cls, lines) -> dict[str, int]:
+ """Extract the nouns from a pytest terminal summary line.
+
+ It always returns the plural noun for consistency::
+
+ ======= 1 failed, 1 passed, 1 warning, 1 error in 0.13s ====
+
+ Will return ``{"failed": 1, "passed": 1, "warnings": 1, "errors": 1}``.
+ """
+ for line in reversed(lines):
+ if rex_session_duration.search(line):
+ outcomes = rex_outcome.findall(line)
+ ret = {noun: int(count) for (count, noun) in outcomes}
+ break
+ else:
+ raise ValueError("Pytest terminal summary report not found")
+
+ to_plural = {
+ "warning": "warnings",
+ "error": "errors",
+ }
+ return {to_plural.get(k, k): v for k, v in ret.items()}
+
+ def assert_outcomes(
+ self,
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+ errors: int = 0,
+ xpassed: int = 0,
+ xfailed: int = 0,
+ warnings: int | None = None,
+ deselected: int | None = None,
+ ) -> None:
+ """
+ Assert that the specified outcomes appear with the respective
+ numbers (0 means it didn't occur) in the text output from a test run.
+
+ ``warnings`` and ``deselected`` are only checked if not None.
+ """
+ __tracebackhide__ = True
+ from _pytest.pytester_assertions import assert_outcomes
+
+ outcomes = self.parseoutcomes()
+ assert_outcomes(
+ outcomes,
+ passed=passed,
+ skipped=skipped,
+ failed=failed,
+ errors=errors,
+ xpassed=xpassed,
+ xfailed=xfailed,
+ warnings=warnings,
+ deselected=deselected,
+ )
+
+
+class SysModulesSnapshot:
+ def __init__(self, preserve: Callable[[str], bool] | None = None) -> None:
+ self.__preserve = preserve
+ self.__saved = dict(sys.modules)
+
+ def restore(self) -> None:
+ if self.__preserve:
+ self.__saved.update(
+ (k, m) for k, m in sys.modules.items() if self.__preserve(k)
+ )
+ sys.modules.clear()
+ sys.modules.update(self.__saved)
+
+
+class SysPathsSnapshot:
+ def __init__(self) -> None:
+ self.__saved = list(sys.path), list(sys.meta_path)
+
+ def restore(self) -> None:
+ sys.path[:], sys.meta_path[:] = self.__saved
+
+
+_FileContent = tuple[str | bytes, ...] | list[str | bytes] | str | bytes
+
+
+@final
+class Pytester:
+ """
+ Facilities to write tests/configuration files, execute pytest in isolation, and match
+ against expected output, perfect for black-box testing of pytest plugins.
+
+ It attempts to isolate the test run from external factors as much as possible, modifying
+ the current working directory to :attr:`path` and environment variables during initialization.
+ """
+
+ __test__ = False
+
+ CLOSE_STDIN: Final = NOTSET
+
+ class TimeoutExpired(Exception):
+ pass
+
+ def __init__(
+ self,
+ request: FixtureRequest,
+ tmp_path_factory: TempPathFactory,
+ monkeypatch: MonkeyPatch,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._request = request
+ self._mod_collections: WeakKeyDictionary[Collector, list[Item | Collector]] = (
+ WeakKeyDictionary()
+ )
+ if request.function:
+ name: str = request.function.__name__
+ else:
+ name = request.node.name
+ self._name = name
+ self._path: Path = tmp_path_factory.mktemp(name, numbered=True)
+ #: A list of plugins to use with :py:meth:`parseconfig` and
+ #: :py:meth:`runpytest`. Initially this is an empty list but plugins can
+ #: be added to the list.
+ #:
+ #: When running in subprocess mode, specify plugins by name (str) - adding
+ #: plugin objects directly is not supported.
+ self.plugins: list[str | _PluggyPlugin] = []
+ self._sys_path_snapshot = SysPathsSnapshot()
+ self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
+ self._request.addfinalizer(self._finalize)
+ self._method = self._request.config.getoption("--runpytest")
+ self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
+
+ self._monkeypatch = mp = monkeypatch
+ self.chdir()
+ mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
+ # Ensure no unexpected caching via tox.
+ mp.delenv("TOX_ENV_DIR", raising=False)
+ # Discard outer pytest options.
+ mp.delenv("PYTEST_ADDOPTS", raising=False)
+ # Ensure no user config is used.
+ tmphome = str(self.path)
+ mp.setenv("HOME", tmphome)
+ mp.setenv("USERPROFILE", tmphome)
+ # Do not use colors for inner runs by default.
+ mp.setenv("PY_COLORS", "0")
+
+ @property
+ def path(self) -> Path:
+ """Temporary directory path used to create files/run tests from, etc."""
+ return self._path
+
+ def __repr__(self) -> str:
+ return f""
+
+ def _finalize(self) -> None:
+ """
+ Clean up global state artifacts.
+
+ Some methods modify the global interpreter state and this tries to
+ clean this up. It does not remove the temporary directory however so
+ it can be looked at after the test run has finished.
+ """
+ self._sys_modules_snapshot.restore()
+ self._sys_path_snapshot.restore()
+
+ def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
+ # Some zope modules used by twisted-related tests keep internal state
+ # and can't be deleted; we had some trouble in the past with
+ # `zope.interface` for example.
+ #
+ # Preserve readline due to https://bugs.python.org/issue41033.
+ # pexpect issues a SIGWINCH.
+ def preserve_module(name):
+ return name.startswith(("zope", "readline"))
+
+ return SysModulesSnapshot(preserve=preserve_module)
+
+ def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
+ """Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
+ pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
+ self._request.addfinalizer(reprec.finish_recording)
+ return reprec
+
+ def chdir(self) -> None:
+ """Cd into the temporary directory.
+
+ This is done automatically upon instantiation.
+ """
+ self._monkeypatch.chdir(self.path)
+
+ def _makefile(
+ self,
+ ext: str,
+ lines: Sequence[Any | bytes],
+ files: Mapping[str, _FileContent],
+ encoding: str = "utf-8",
+ ) -> Path:
+ items = list(files.items())
+
+ if ext is None:
+ raise TypeError("ext must not be None")
+
+ if ext and not ext.startswith("."):
+ raise ValueError(
+ f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"
+ )
+
+ def to_text(s: Any | bytes) -> str:
+ return s.decode(encoding) if isinstance(s, bytes) else str(s)
+
+ if lines:
+ source = "\n".join(to_text(x) for x in lines)
+ basename = self._name
+ items.insert(0, (basename, source))
+
+ ret = None
+ for basename, value in items:
+ p = self.path.joinpath(basename).with_suffix(ext)
+ p.parent.mkdir(parents=True, exist_ok=True)
+ source_ = Source(value)
+ source = "\n".join(to_text(line) for line in source_.lines)
+ p.write_text(source.strip(), encoding=encoding)
+ if ret is None:
+ ret = p
+ assert ret is not None
+ return ret
+
+ def makefile(self, ext: str, *args: str, **kwargs: str) -> Path:
+ r"""Create new text file(s) in the test directory.
+
+ :param ext:
+ The extension the file(s) should use, including the dot, e.g. `.py`.
+ :param args:
+ All args are treated as strings and joined using newlines.
+ The result is written as contents to the file. The name of the
+ file is based on the test function requesting this fixture.
+ :param kwargs:
+ Each keyword is the name of a file, while the value of it will
+ be written as contents of the file.
+ :returns:
+ The first created file.
+
+ Examples:
+
+ .. code-block:: python
+
+ pytester.makefile(".txt", "line1", "line2")
+
+ pytester.makefile(".ini", pytest="[pytest]\naddopts=-rs\n")
+
+ To create binary files, use :meth:`pathlib.Path.write_bytes` directly:
+
+ .. code-block:: python
+
+ filename = pytester.path.joinpath("foo.bin")
+ filename.write_bytes(b"...")
+ """
+ return self._makefile(ext, args, kwargs)
+
+ def makeconftest(self, source: str) -> Path:
+ """Write a conftest.py file.
+
+ :param source: The contents.
+ :returns: The conftest.py file.
+ """
+ return self.makepyfile(conftest=source)
+
+ def makeini(self, source: str) -> Path:
+ """Write a tox.ini file.
+
+ :param source: The contents.
+ :returns: The tox.ini file.
+ """
+ return self.makefile(".ini", tox=source)
+
+ def maketoml(self, source: str) -> Path:
+ """Write a pytest.toml file.
+
+ :param source: The contents.
+ :returns: The pytest.toml file.
+
+ .. versionadded:: 9.0
+ """
+ return self.makefile(".toml", pytest=source)
+
+ def getinicfg(self, source: str) -> SectionWrapper:
+ """Return the pytest section from the tox.ini config file."""
+ p = self.makeini(source)
+ return IniConfig(str(p))["pytest"]
+
+ def makepyprojecttoml(self, source: str) -> Path:
+ """Write a pyproject.toml file.
+
+ :param source: The contents.
+ :returns: The pyproject.ini file.
+
+ .. versionadded:: 6.0
+ """
+ return self.makefile(".toml", pyproject=source)
+
+ def makepyfile(self, *args: _FileContent, **kwargs: _FileContent) -> Path:
+ r"""Shortcut for .makefile() with a .py extension.
+
+ Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
+ existing files.
+
+ Examples:
+
+ .. code-block:: python
+
+ def test_something(pytester):
+ # Initial file is created test_something.py.
+ pytester.makepyfile("foobar")
+ # To create multiple files, pass kwargs accordingly.
+ pytester.makepyfile(custom="foobar")
+ # At this point, both 'test_something.py' & 'custom.py' exist in the test directory.
+
+ """
+ return self._makefile(".py", args, kwargs)
+
+ def maketxtfile(self, *args: _FileContent, **kwargs: _FileContent) -> Path:
+ r"""Shortcut for .makefile() with a .txt extension.
+
+ Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
+ existing files.
+
+ Examples:
+
+ .. code-block:: python
+
+ def test_something(pytester):
+ # Initial file is created test_something.txt.
+ pytester.maketxtfile("foobar")
+ # To create multiple files, pass kwargs accordingly.
+ pytester.maketxtfile(custom="foobar")
+ # At this point, both 'test_something.txt' & 'custom.txt' exist in the test directory.
+
+ """
+ return self._makefile(".txt", args, kwargs)
+
+ def syspathinsert(self, path: str | os.PathLike[str] | None = None) -> None:
+ """Prepend a directory to sys.path, defaults to :attr:`path`.
+
+ This is undone automatically when this object dies at the end of each
+ test.
+
+ :param path:
+ The path.
+ """
+ if path is None:
+ path = self.path
+
+ self._monkeypatch.syspath_prepend(str(path))
+
+ def mkdir(self, name: str | os.PathLike[str]) -> Path:
+ """Create a new (sub)directory.
+
+ :param name:
+ The name of the directory, relative to the pytester path.
+ :returns:
+ The created directory.
+ :rtype: pathlib.Path
+ """
+ p = self.path / name
+ p.mkdir()
+ return p
+
+ def mkpydir(self, name: str | os.PathLike[str]) -> Path:
+ """Create a new python package.
+
+ This creates a (sub)directory with an empty ``__init__.py`` file so it
+ gets recognised as a Python package.
+ """
+ p = self.path / name
+ p.mkdir()
+ p.joinpath("__init__.py").touch()
+ return p
+
+ def copy_example(self, name: str | None = None) -> Path:
+ """Copy file from project's directory into the testdir.
+
+ :param name:
+ The name of the file to copy.
+ :return:
+ Path to the copied directory (inside ``self.path``).
+ :rtype: pathlib.Path
+ """
+ example_dir_ = self._request.config.getini("pytester_example_dir")
+ if example_dir_ is None:
+ raise ValueError("pytester_example_dir is unset, can't copy examples")
+ example_dir: Path = self._request.config.rootpath / example_dir_
+
+ for extra_element in self._request.node.iter_markers("pytester_example_path"):
+ assert extra_element.args
+ example_dir = example_dir.joinpath(*extra_element.args)
+
+ if name is None:
+ func_name = self._name
+ maybe_dir = example_dir / func_name
+ maybe_file = example_dir / (func_name + ".py")
+
+ if maybe_dir.is_dir():
+ example_path = maybe_dir
+ elif maybe_file.is_file():
+ example_path = maybe_file
+ else:
+ raise LookupError(
+ f"{func_name} can't be found as module or package in {example_dir}"
+ )
+ else:
+ example_path = example_dir.joinpath(name)
+
+ if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
+ shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True)
+ return self.path
+ elif example_path.is_file():
+ result = self.path.joinpath(example_path.name)
+ shutil.copy(example_path, result)
+ return result
+ else:
+ raise LookupError(
+ f'example "{example_path}" is not found as a file or directory'
+ )
+
+ def getnode(self, config: Config, arg: str | os.PathLike[str]) -> Collector | Item:
+ """Get the collection node of a file.
+
+ :param config:
+ A pytest config.
+ See :py:meth:`parseconfig` and :py:meth:`parseconfigure` for creating it.
+ :param arg:
+ Path to the file.
+ :returns:
+ The node.
+ """
+ session = Session.from_config(config)
+ assert "::" not in str(arg)
+ p = Path(os.path.abspath(arg))
+ config.hook.pytest_sessionstart(session=session)
+ res = session.perform_collect([str(p)], genitems=False)[0]
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
+ return res
+
+ def getpathnode(self, path: str | os.PathLike[str]) -> Collector | Item:
+ """Return the collection node of a file.
+
+ This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
+ create the (configured) pytest Config instance.
+
+ :param path:
+ Path to the file.
+ :returns:
+ The node.
+ """
+ path = Path(path)
+ config = self.parseconfigure(path)
+ session = Session.from_config(config)
+ x = bestrelpath(session.path, path)
+ config.hook.pytest_sessionstart(session=session)
+ res = session.perform_collect([x], genitems=False)[0]
+ config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
+ return res
+
+ def genitems(self, colitems: Sequence[Item | Collector]) -> list[Item]:
+ """Generate all test items from a collection node.
+
+ This recurses into the collection node and returns a list of all the
+ test items contained within.
+
+ :param colitems:
+ The collection nodes.
+ :returns:
+ The collected items.
+ """
+ session = colitems[0].session
+ result: list[Item] = []
+ for colitem in colitems:
+ result.extend(session.genitems(colitem))
+ return result
+
+ def runitem(self, source: str) -> Any:
+ """Run the "test_func" Item.
+
+ The calling test instance (class containing the test method) must
+ provide a ``.getrunner()`` method which should return a runner which
+ can run the test protocol for a single item, e.g.
+ ``_pytest.runner.runtestprotocol``.
+ """
+ # used from runner functional tests
+ item = self.getitem(source)
+ # the test class where we are called from wants to provide the runner
+ testclassinstance = self._request.instance
+ runner = testclassinstance.getrunner()
+ return runner(item)
+
+ def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder:
+ """Run a test module in process using ``pytest.main()``.
+
+ This run writes "source" into a temporary file and runs
+ ``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
+ for the result.
+
+ :param source: The source code of the test module.
+ :param cmdlineargs: Any extra command line arguments to use.
+ """
+ p = self.makepyfile(source)
+ values = [*list(cmdlineargs), p]
+ return self.inline_run(*values)
+
+ def inline_genitems(self, *args) -> tuple[list[Item], HookRecorder]:
+ """Run ``pytest.main(['--collect-only'])`` in-process.
+
+ Runs the :py:func:`pytest.main` function to run all of pytest inside
+ the test process itself like :py:meth:`inline_run`, but returns a
+ tuple of the collected items and a :py:class:`HookRecorder` instance.
+ """
+ rec = self.inline_run("--collect-only", *args)
+ items = [x.item for x in rec.getcalls("pytest_itemcollected")]
+ return items, rec
+
+ def inline_run(
+ self,
+ *args: str | os.PathLike[str],
+ plugins=(),
+ no_reraise_ctrlc: bool = False,
+ ) -> HookRecorder:
+ """Run ``pytest.main()`` in-process, returning a HookRecorder.
+
+ Runs the :py:func:`pytest.main` function to run all of pytest inside
+ the test process itself. This means it can return a
+ :py:class:`HookRecorder` instance which gives more detailed results
+ from that run than can be done by matching stdout/stderr from
+ :py:meth:`runpytest`.
+
+ :param args:
+ Command line arguments to pass to :py:func:`pytest.main`.
+ :param plugins:
+ Extra plugin instances the ``pytest.main()`` instance should use.
+ :param no_reraise_ctrlc:
+ Typically we reraise keyboard interrupts from the child run. If
+ True, the KeyboardInterrupt exception is captured.
+ """
+ from _pytest.unraisableexception import gc_collect_iterations_key
+
+ # (maybe a cpython bug?) the importlib cache sometimes isn't updated
+ # properly between file creation and inline_run (especially if imports
+ # are interspersed with file creation)
+ importlib.invalidate_caches()
+
+ plugins = list(plugins)
+ finalizers = []
+ try:
+ # Any sys.module or sys.path changes done while running pytest
+ # inline should be reverted after the test run completes to avoid
+ # clashing with later inline tests run within the same pytest test,
+ # e.g. just because they use matching test module names.
+ finalizers.append(self.__take_sys_modules_snapshot().restore)
+ finalizers.append(SysPathsSnapshot().restore)
+
+ # Important note:
+ # - our tests should not leave any other references/registrations
+ # laying around other than possibly loaded test modules
+ # referenced from sys.modules, as nothing will clean those up
+ # automatically
+
+ rec = []
+
+ class PytesterHelperPlugin:
+ @staticmethod
+ def pytest_configure(config: Config) -> None:
+ rec.append(self.make_hook_recorder(config.pluginmanager))
+
+ # The unraisable plugin GC collect slows down inline
+ # pytester runs too much.
+ config.stash[gc_collect_iterations_key] = 0
+
+ plugins.append(PytesterHelperPlugin())
+ ret = main([str(x) for x in args], plugins=plugins)
+ if len(rec) == 1:
+ reprec = rec.pop()
+ else:
+
+ class reprec: # type: ignore
+ pass
+
+ reprec.ret = ret
+
+ # Typically we reraise keyboard interrupts from the child run
+ # because it's our user requesting interruption of the testing.
+ if ret == ExitCode.INTERRUPTED and not no_reraise_ctrlc:
+ calls = reprec.getcalls("pytest_keyboard_interrupt")
+ if calls and calls[-1].excinfo.type == KeyboardInterrupt:
+ raise KeyboardInterrupt()
+ return reprec
+ finally:
+ for finalizer in finalizers:
+ finalizer()
+
+ def runpytest_inprocess(
+ self, *args: str | os.PathLike[str], **kwargs: Any
+ ) -> RunResult:
+ """Return result of running pytest in-process, providing a similar
+ interface to what self.runpytest() provides."""
+ syspathinsert = kwargs.pop("syspathinsert", False)
+
+ if syspathinsert:
+ self.syspathinsert()
+ instant = timing.Instant()
+ capture = _get_multicapture("sys")
+ capture.start_capturing()
+ try:
+ try:
+ reprec = self.inline_run(*args, **kwargs)
+ except SystemExit as e:
+ ret = e.args[0]
+ try:
+ ret = ExitCode(e.args[0])
+ except ValueError:
+ pass
+
+ class reprec: # type: ignore
+ ret = ret
+
+ except Exception:
+ traceback.print_exc()
+
+ class reprec: # type: ignore
+ ret = ExitCode(3)
+
+ finally:
+ out, err = capture.readouterr()
+ capture.stop_capturing()
+ sys.stdout.write(out)
+ sys.stderr.write(err)
+
+ assert reprec.ret is not None
+ res = RunResult(
+ reprec.ret, out.splitlines(), err.splitlines(), instant.elapsed().seconds
+ )
+ res.reprec = reprec # type: ignore
+ return res
+
+ def runpytest(self, *args: str | os.PathLike[str], **kwargs: Any) -> RunResult:
+ """Run pytest inline or in a subprocess, depending on the command line
+ option "--runpytest" and return a :py:class:`~pytest.RunResult`."""
+ new_args = self._ensure_basetemp(args)
+ if self._method == "inprocess":
+ return self.runpytest_inprocess(*new_args, **kwargs)
+ elif self._method == "subprocess":
+ return self.runpytest_subprocess(*new_args, **kwargs)
+ raise RuntimeError(f"Unrecognized runpytest option: {self._method}")
+
+ def _ensure_basetemp(
+ self, args: Sequence[str | os.PathLike[str]]
+ ) -> list[str | os.PathLike[str]]:
+ new_args = list(args)
+ for x in new_args:
+ if str(x).startswith("--basetemp"):
+ break
+ else:
+ new_args.append(
+ "--basetemp={}".format(self.path.parent.joinpath("basetemp"))
+ )
+ return new_args
+
+ def parseconfig(self, *args: str | os.PathLike[str]) -> Config:
+ """Return a new pytest :class:`pytest.Config` instance from given
+ commandline args.
+
+ This invokes the pytest bootstrapping code in _pytest.config to create a
+ new :py:class:`pytest.PytestPluginManager` and call the
+ :hook:`pytest_cmdline_parse` hook to create a new :class:`pytest.Config`
+ instance.
+
+ If :attr:`plugins` has been populated they should be plugin modules
+ to be registered with the plugin manager.
+ """
+ import _pytest.config
+
+ new_args = [str(x) for x in self._ensure_basetemp(args)]
+
+ config = _pytest.config._prepareconfig(new_args, self.plugins)
+ # we don't know what the test will do with this half-setup config
+ # object and thus we make sure it gets unconfigured properly in any
+ # case (otherwise capturing could still be active, for example)
+ self._request.addfinalizer(config._ensure_unconfigure)
+ return config
+
+ def parseconfigure(self, *args: str | os.PathLike[str]) -> Config:
+ """Return a new pytest configured Config instance.
+
+ Returns a new :py:class:`pytest.Config` instance like
+ :py:meth:`parseconfig`, but also calls the :hook:`pytest_configure`
+ hook.
+ """
+ config = self.parseconfig(*args)
+ config._do_configure()
+ return config
+
+ def getitem(
+ self, source: str | os.PathLike[str], funcname: str = "test_func"
+ ) -> Item:
+ """Return the test item for a test function.
+
+ Writes the source to a python file and runs pytest's collection on
+ the resulting module, returning the test item for the requested
+ function name.
+
+ :param source:
+ The module source.
+ :param funcname:
+ The name of the test function for which to return a test item.
+ :returns:
+ The test item.
+ """
+ items = self.getitems(source)
+ for item in items:
+ if item.name == funcname:
+ return item
+ assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}"
+
+ def getitems(self, source: str | os.PathLike[str]) -> list[Item]:
+ """Return all test items collected from the module.
+
+ Writes the source to a Python file and runs pytest's collection on
+ the resulting module, returning all test items contained within.
+ """
+ modcol = self.getmodulecol(source)
+ return self.genitems([modcol])
+
+ def getmodulecol(
+ self,
+ source: str | os.PathLike[str],
+ configargs=(),
+ *,
+ withinit: bool = False,
+ ):
+ """Return the module collection node for ``source``.
+
+ Writes ``source`` to a file using :py:meth:`makepyfile` and then
+ runs the pytest collection on it, returning the collection node for the
+ test module.
+
+ :param source:
+ The source code of the module to collect.
+
+ :param configargs:
+ Any extra arguments to pass to :py:meth:`parseconfigure`.
+
+ :param withinit:
+ Whether to also write an ``__init__.py`` file to the same
+ directory to ensure it is a package.
+ """
+ if isinstance(source, os.PathLike):
+ path = self.path.joinpath(source)
+ assert not withinit, "not supported for paths"
+ else:
+ kw = {self._name: str(source)}
+ path = self.makepyfile(**kw)
+ if withinit:
+ self.makepyfile(__init__="#")
+ self.config = config = self.parseconfigure(path, *configargs)
+ return self.getnode(config, path)
+
+ def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None:
+ """Return the collection node for name from the module collection.
+
+ Searches a module collection node for a collection node matching the
+ given name.
+
+ :param modcol: A module collection node; see :py:meth:`getmodulecol`.
+ :param name: The name of the node to return.
+ """
+ if modcol not in self._mod_collections:
+ self._mod_collections[modcol] = list(modcol.collect())
+ for colitem in self._mod_collections[modcol]:
+ if colitem.name == name:
+ return colitem
+ return None
+
+ def popen(
+ self,
+ cmdargs: Sequence[str | os.PathLike[str]],
+ stdout: int | TextIO = subprocess.PIPE,
+ stderr: int | TextIO = subprocess.PIPE,
+ stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN,
+ **kw,
+ ):
+ """Invoke :py:class:`subprocess.Popen`.
+
+ Calls :py:class:`subprocess.Popen` making sure the current working
+ directory is in ``PYTHONPATH``.
+
+ You probably want to use :py:meth:`run` instead.
+ """
+ env = os.environ.copy()
+ env["PYTHONPATH"] = os.pathsep.join(
+ filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
+ )
+ kw["env"] = env
+
+ if stdin is self.CLOSE_STDIN:
+ kw["stdin"] = subprocess.PIPE
+ elif isinstance(stdin, bytes):
+ kw["stdin"] = subprocess.PIPE
+ else:
+ kw["stdin"] = stdin
+
+ popen = subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
+ if stdin is self.CLOSE_STDIN:
+ assert popen.stdin is not None
+ popen.stdin.close()
+ elif isinstance(stdin, bytes):
+ assert popen.stdin is not None
+ popen.stdin.write(stdin)
+
+ return popen
+
+ def run(
+ self,
+ *cmdargs: str | os.PathLike[str],
+ timeout: float | None = None,
+ stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN,
+ ) -> RunResult:
+ """Run a command with arguments.
+
+ Run a process using :py:class:`subprocess.Popen` saving the stdout and
+ stderr.
+
+ :param cmdargs:
+ The sequence of arguments to pass to :py:class:`subprocess.Popen`,
+ with path-like objects being converted to :py:class:`str`
+ automatically.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Pytester.TimeoutExpired`.
+ :param stdin:
+ Optional standard input.
+
+ - If it is ``CLOSE_STDIN`` (Default), then this method calls
+ :py:class:`subprocess.Popen` with ``stdin=subprocess.PIPE``, and
+ the standard input is closed immediately after the new command is
+ started.
+
+ - If it is of type :py:class:`bytes`, these bytes are sent to the
+ standard input of the command.
+
+ - Otherwise, it is passed through to :py:class:`subprocess.Popen`.
+ For further information in this case, consult the document of the
+ ``stdin`` parameter in :py:class:`subprocess.Popen`.
+ :type stdin: _pytest.compat.NotSetType | bytes | IO[Any] | int
+ :returns:
+ The result.
+
+ """
+ __tracebackhide__ = True
+
+ cmdargs = tuple(os.fspath(arg) for arg in cmdargs)
+ p1 = self.path.joinpath("stdout")
+ p2 = self.path.joinpath("stderr")
+ print("running:", *cmdargs)
+ print(" in:", Path.cwd())
+
+ with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2:
+ instant = timing.Instant()
+ popen = self.popen(
+ cmdargs,
+ stdin=stdin,
+ stdout=f1,
+ stderr=f2,
+ )
+ if popen.stdin is not None:
+ popen.stdin.close()
+
+ def handle_timeout() -> None:
+ __tracebackhide__ = True
+
+ timeout_message = f"{timeout} second timeout expired running: {cmdargs}"
+
+ popen.kill()
+ popen.wait()
+ raise self.TimeoutExpired(timeout_message)
+
+ if timeout is None:
+ ret = popen.wait()
+ else:
+ try:
+ ret = popen.wait(timeout)
+ except subprocess.TimeoutExpired:
+ handle_timeout()
+ f1.flush()
+ f2.flush()
+
+ with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2:
+ out = f1.read().splitlines()
+ err = f2.read().splitlines()
+
+ self._dump_lines(out, sys.stdout)
+ self._dump_lines(err, sys.stderr)
+
+ with contextlib.suppress(ValueError):
+ ret = ExitCode(ret)
+ return RunResult(ret, out, err, instant.elapsed().seconds)
+
+ def _dump_lines(self, lines, fp):
+ try:
+ for line in lines:
+ print(line, file=fp)
+ except UnicodeEncodeError:
+ print(f"couldn't print to {fp} because of encoding")
+
+ def _getpytestargs(self) -> tuple[str, ...]:
+ return sys.executable, "-mpytest"
+
+ def runpython(self, script: os.PathLike[str]) -> RunResult:
+ """Run a python script using sys.executable as interpreter."""
+ return self.run(sys.executable, script)
+
+ def runpython_c(self, command: str) -> RunResult:
+ """Run ``python -c "command"``."""
+ return self.run(sys.executable, "-c", command)
+
+ def runpytest_subprocess(
+ self, *args: str | os.PathLike[str], timeout: float | None = None
+ ) -> RunResult:
+ """Run pytest as a subprocess with given arguments.
+
+ Any plugins added to the :py:attr:`plugins` list will be added using the
+ ``-p`` command line option. Additionally ``--basetemp`` is used to put
+ any temporary files and directories in a numbered directory prefixed
+ with "runpytest-" to not conflict with the normal numbered pytest
+ location for temporary files and directories.
+
+ :param args:
+ The sequence of arguments to pass to the pytest subprocess.
+ :param timeout:
+ The period in seconds after which to timeout and raise
+ :py:class:`Pytester.TimeoutExpired`.
+ :returns:
+ The result.
+ """
+ __tracebackhide__ = True
+ p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
+ args = (f"--basetemp={p}", *args)
+ for plugin in self.plugins:
+ if not isinstance(plugin, str):
+ raise ValueError(
+ f"Specifying plugins as objects is not supported in pytester subprocess mode; "
+ f"specify by name instead: {plugin}"
+ )
+ args = ("-p", plugin, *args)
+ args = self._getpytestargs() + args
+ return self.run(*args, timeout=timeout)
+
+ def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn:
+ """Run pytest using pexpect.
+
+ This makes sure to use the right pytest and sets up the temporary
+ directory locations.
+
+ The pexpect child is returned.
+ """
+ basetemp = self.path / "temp-pexpect"
+ basetemp.mkdir(mode=0o700)
+ invoke = " ".join(map(str, self._getpytestargs()))
+ cmd = f"{invoke} --basetemp={basetemp} {string}"
+ return self.spawn(cmd, expect_timeout=expect_timeout)
+
+ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn:
+ """Run a command using pexpect.
+
+ The pexpect child is returned.
+ """
+ pexpect = importorskip("pexpect", "3.0")
+ if hasattr(sys, "pypy_version_info") and "64" in platform.machine():
+ skip("pypy-64 bit not supported")
+ if not hasattr(pexpect, "spawn"):
+ skip("pexpect.spawn not available")
+ logfile = self.path.joinpath("spawn.out").open("wb")
+
+ child = pexpect.spawn(cmd, logfile=logfile, timeout=expect_timeout)
+ self._request.addfinalizer(logfile.close)
+ return child
+
+
+class LineComp:
+ def __init__(self) -> None:
+ self.stringio = StringIO()
+ """:class:`python:io.StringIO()` instance used for input."""
+
+ def assert_contains_lines(self, lines2: Sequence[str]) -> None:
+ """Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
+
+ Lines are matched using :func:`LineMatcher.fnmatch_lines `.
+ """
+ __tracebackhide__ = True
+ val = self.stringio.getvalue()
+ self.stringio.truncate(0)
+ self.stringio.seek(0)
+ lines1 = val.split("\n")
+ LineMatcher(lines1).fnmatch_lines(lines2)
+
+
+class LineMatcher:
+ """Flexible matching of text.
+
+ This is a convenience class to test large texts like the output of
+ commands.
+
+ The constructor takes a list of lines without their trailing newlines, i.e.
+ ``text.splitlines()``.
+ """
+
+ def __init__(self, lines: list[str]) -> None:
+ self.lines = lines
+ self._log_output: list[str] = []
+
+ def __str__(self) -> str:
+ """Return the entire original text.
+
+ .. versionadded:: 6.2
+ You can use :meth:`str` in older versions.
+ """
+ return "\n".join(self.lines)
+
+ def _getlines(self, lines2: str | Sequence[str] | Source) -> Sequence[str]:
+ if isinstance(lines2, str):
+ lines2 = Source(lines2)
+ if isinstance(lines2, Source):
+ lines2 = lines2.strip().lines
+ return lines2
+
+ def fnmatch_lines_random(self, lines2: Sequence[str]) -> None:
+ """Check lines exist in the output in any order (using :func:`python:fnmatch.fnmatch`)."""
+ __tracebackhide__ = True
+ self._match_lines_random(lines2, fnmatch)
+
+ def re_match_lines_random(self, lines2: Sequence[str]) -> None:
+ """Check lines exist in the output in any order (using :func:`python:re.match`)."""
+ __tracebackhide__ = True
+ self._match_lines_random(lines2, lambda name, pat: bool(re.match(pat, name)))
+
+ def _match_lines_random(
+ self, lines2: Sequence[str], match_func: Callable[[str, str], bool]
+ ) -> None:
+ __tracebackhide__ = True
+ lines2 = self._getlines(lines2)
+ for line in lines2:
+ for x in self.lines:
+ if line == x or match_func(x, line):
+ self._log("matched: ", repr(line))
+ break
+ else:
+ msg = f"line {line!r} not found in output"
+ self._log(msg)
+ self._fail(msg)
+
+ def get_lines_after(self, fnline: str) -> Sequence[str]:
+ """Return all lines following the given line in the text.
+
+ The given line can contain glob wildcards.
+ """
+ for i, line in enumerate(self.lines):
+ if fnline == line or fnmatch(line, fnline):
+ return self.lines[i + 1 :]
+ raise ValueError(f"line {fnline!r} not found in output")
+
+ def _log(self, *args) -> None:
+ self._log_output.append(" ".join(str(x) for x in args))
+
+ @property
+ def _log_text(self) -> str:
+ return "\n".join(self._log_output)
+
+ def fnmatch_lines(
+ self, lines2: Sequence[str], *, consecutive: bool = False
+ ) -> None:
+ """Check lines exist in the output (using :func:`python:fnmatch.fnmatch`).
+
+ The argument is a list of lines which have to match and can use glob
+ wildcards. If they do not match a pytest.fail() is called. The
+ matches and non-matches are also shown as part of the error message.
+
+ :param lines2: String patterns to match.
+ :param consecutive: Match lines consecutively?
+ """
+ __tracebackhide__ = True
+ self._match_lines(lines2, fnmatch, "fnmatch", consecutive=consecutive)
+
+ def re_match_lines(
+ self, lines2: Sequence[str], *, consecutive: bool = False
+ ) -> None:
+ """Check lines exist in the output (using :func:`python:re.match`).
+
+ The argument is a list of lines which have to match using ``re.match``.
+ If they do not match a pytest.fail() is called.
+
+ The matches and non-matches are also shown as part of the error message.
+
+ :param lines2: string patterns to match.
+ :param consecutive: match lines consecutively?
+ """
+ __tracebackhide__ = True
+ self._match_lines(
+ lines2,
+ lambda name, pat: bool(re.match(pat, name)),
+ "re.match",
+ consecutive=consecutive,
+ )
+
+ def _match_lines(
+ self,
+ lines2: Sequence[str],
+ match_func: Callable[[str, str], bool],
+ match_nickname: str,
+ *,
+ consecutive: bool = False,
+ ) -> None:
+ """Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
+
+ :param Sequence[str] lines2:
+ List of string patterns to match. The actual format depends on
+ ``match_func``.
+ :param match_func:
+ A callable ``match_func(line, pattern)`` where line is the
+ captured line from stdout/stderr and pattern is the matching
+ pattern.
+ :param str match_nickname:
+ The nickname for the match function that will be logged to stdout
+ when a match occurs.
+ :param consecutive:
+ Match lines consecutively?
+ """
+ if not isinstance(lines2, collections.abc.Sequence):
+ raise TypeError(f"invalid type for lines2: {type(lines2).__name__}")
+ lines2 = self._getlines(lines2)
+ lines1 = self.lines[:]
+ extralines = []
+ __tracebackhide__ = True
+ wnick = len(match_nickname) + 1
+ started = False
+ for line in lines2:
+ nomatchprinted = False
+ while lines1:
+ nextline = lines1.pop(0)
+ if line == nextline:
+ self._log("exact match:", repr(line))
+ started = True
+ break
+ elif match_func(nextline, line):
+ self._log(f"{match_nickname}:", repr(line))
+ self._log(
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
+ )
+ started = True
+ break
+ else:
+ if consecutive and started:
+ msg = f"no consecutive match: {line!r}"
+ self._log(msg)
+ self._log(
+ "{:>{width}}".format("with:", width=wnick), repr(nextline)
+ )
+ self._fail(msg)
+ if not nomatchprinted:
+ self._log(
+ "{:>{width}}".format("nomatch:", width=wnick), repr(line)
+ )
+ nomatchprinted = True
+ self._log("{:>{width}}".format("and:", width=wnick), repr(nextline))
+ extralines.append(nextline)
+ else:
+ msg = f"remains unmatched: {line!r}"
+ self._log(msg)
+ self._fail(msg)
+ self._log_output = []
+
+ def no_fnmatch_line(self, pat: str) -> None:
+ """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``.
+
+ :param str pat: The pattern to match lines.
+ """
+ __tracebackhide__ = True
+ self._no_match_line(pat, fnmatch, "fnmatch")
+
+ def no_re_match_line(self, pat: str) -> None:
+ """Ensure captured lines do not match the given pattern, using ``re.match``.
+
+ :param str pat: The regular expression to match lines.
+ """
+ __tracebackhide__ = True
+ self._no_match_line(
+ pat, lambda name, pat: bool(re.match(pat, name)), "re.match"
+ )
+
+ def _no_match_line(
+ self, pat: str, match_func: Callable[[str, str], bool], match_nickname: str
+ ) -> None:
+ """Underlying implementation of ``no_fnmatch_line`` and ``no_re_match_line``.
+
+ :param str pat:
+ The pattern to match lines.
+ :param match_func:
+ A callable ``match_func(line, pattern)`` where line is the
+ captured line from stdout/stderr and pattern is the matching
+ pattern.
+ :param match_nickname:
+ The nickname for the match function that will be logged to stdout
+ when a match occurs.
+ """
+ __tracebackhide__ = True
+ nomatch_printed = False
+ wnick = len(match_nickname) + 1
+ for line in self.lines:
+ if match_func(line, pat):
+ msg = f"{match_nickname}: {pat!r}"
+ self._log(msg)
+ self._log("{:>{width}}".format("with:", width=wnick), repr(line))
+ self._fail(msg)
+ else:
+ if not nomatch_printed:
+ self._log("{:>{width}}".format("nomatch:", width=wnick), repr(pat))
+ nomatch_printed = True
+ self._log("{:>{width}}".format("and:", width=wnick), repr(line))
+ self._log_output = []
+
+ def _fail(self, msg: str) -> None:
+ __tracebackhide__ = True
+ log_text = self._log_text
+ self._log_output = []
+ fail(log_text)
+
+ def str(self) -> str:
+ """Return the entire original text."""
+ return str(self)
diff --git a/venv/lib/python3.11/site-packages/_pytest/pytester_assertions.py b/venv/lib/python3.11/site-packages/_pytest/pytester_assertions.py
new file mode 100644
index 000000000..b8d8a1952
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/pytester_assertions.py
@@ -0,0 +1,74 @@
+"""Helper plugin for pytester; should not be loaded on its own."""
+
+# This plugin contains assertions used by pytester. pytester cannot
+# contain them itself, since it is imported by the `pytest` module,
+# hence cannot be subject to assertion rewriting, which requires a
+# module to not be already imported.
+from __future__ import annotations
+
+from collections.abc import Sequence
+
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+
+
+def assertoutcome(
+ outcomes: tuple[
+ Sequence[TestReport],
+ Sequence[CollectReport | TestReport],
+ Sequence[CollectReport | TestReport],
+ ],
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+) -> None:
+ __tracebackhide__ = True
+
+ realpassed, realskipped, realfailed = outcomes
+ obtained = {
+ "failed": len(realfailed),
+ "passed": len(realpassed),
+ "skipped": len(realskipped),
+ }
+ expected = {"failed": failed, "passed": passed, "skipped": skipped}
+ assert obtained == expected, outcomes
+
+
+def assert_outcomes(
+ outcomes: dict[str, int],
+ passed: int = 0,
+ skipped: int = 0,
+ failed: int = 0,
+ errors: int = 0,
+ xpassed: int = 0,
+ xfailed: int = 0,
+ warnings: int | None = None,
+ deselected: int | None = None,
+) -> None:
+ """Assert that the specified outcomes appear with the respective
+ numbers (0 means it didn't occur) in the text output from a test run."""
+ __tracebackhide__ = True
+
+ obtained = {
+ "passed": outcomes.get("passed", 0),
+ "skipped": outcomes.get("skipped", 0),
+ "failed": outcomes.get("failed", 0),
+ "errors": outcomes.get("errors", 0),
+ "xpassed": outcomes.get("xpassed", 0),
+ "xfailed": outcomes.get("xfailed", 0),
+ }
+ expected = {
+ "passed": passed,
+ "skipped": skipped,
+ "failed": failed,
+ "errors": errors,
+ "xpassed": xpassed,
+ "xfailed": xfailed,
+ }
+ if warnings is not None:
+ obtained["warnings"] = outcomes.get("warnings", 0)
+ expected["warnings"] = warnings
+ if deselected is not None:
+ obtained["deselected"] = outcomes.get("deselected", 0)
+ expected["deselected"] = deselected
+ assert obtained == expected
diff --git a/venv/lib/python3.11/site-packages/_pytest/python.py b/venv/lib/python3.11/site-packages/_pytest/python.py
new file mode 100644
index 000000000..45ff18518
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/python.py
@@ -0,0 +1,1759 @@
+# mypy: allow-untyped-defs
+"""Python test discovery, setup and run of test functions."""
+
+from __future__ import annotations
+
+import abc
+from collections import Counter
+from collections import defaultdict
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Mapping
+from collections.abc import Sequence
+import dataclasses
+import enum
+import fnmatch
+from functools import partial
+import inspect
+import itertools
+import os
+from pathlib import Path
+import re
+import textwrap
+import types
+from typing import Any
+from typing import cast
+from typing import final
+from typing import Literal
+from typing import NoReturn
+from typing import TYPE_CHECKING
+import warnings
+
+import _pytest
+from _pytest import fixtures
+from _pytest import nodes
+from _pytest._code import filter_traceback
+from _pytest._code import getfslineno
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest._code.code import Traceback
+from _pytest._io.saferepr import saferepr
+from _pytest.compat import ascii_escaped
+from _pytest.compat import get_default_arg_names
+from _pytest.compat import get_real_func
+from _pytest.compat import getimfunc
+from _pytest.compat import is_async_function
+from _pytest.compat import NOTSET
+from _pytest.compat import safe_getattr
+from _pytest.compat import safe_isclass
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import _resolve_args_directness
+from _pytest.fixtures import FixtureDef
+from _pytest.fixtures import FixtureRequest
+from _pytest.fixtures import FixtureValue
+from _pytest.fixtures import FuncFixtureInfo
+from _pytest.fixtures import get_scope_node
+from _pytest.main import Session
+from _pytest.mark import ParameterSet
+from _pytest.mark.structures import _HiddenParam
+from _pytest.mark.structures import get_unpacked_marks
+from _pytest.mark.structures import HIDDEN_PARAM
+from _pytest.mark.structures import Mark
+from _pytest.mark.structures import MarkDecorator
+from _pytest.mark.structures import normalize_mark_list
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.pathlib import fnmatch_ex
+from _pytest.pathlib import import_path
+from _pytest.pathlib import ImportPathMismatchError
+from _pytest.pathlib import scandir
+from _pytest.scope import Scope
+from _pytest.scope import ScopeName
+from _pytest.stash import StashKey
+from _pytest.warning_types import PytestCollectionWarning
+from _pytest.warning_types import PytestReturnNotNoneWarning
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "python_files",
+ type="args",
+ # NOTE: default is also used in AssertionRewritingHook.
+ default=["test_*.py", "*_test.py"],
+ help="Glob-style file patterns for Python test module discovery",
+ )
+ parser.addini(
+ "python_classes",
+ type="args",
+ default=["Test"],
+ help="Prefixes or glob names for Python test class discovery",
+ )
+ parser.addini(
+ "python_functions",
+ type="args",
+ default=["test"],
+ help="Prefixes or glob names for Python test function and method discovery",
+ )
+ parser.addini(
+ "disable_test_id_escaping_and_forfeit_all_rights_to_community_support",
+ type="bool",
+ default=False,
+ help="Disable string escape non-ASCII characters, might cause unwanted "
+ "side effects(use at your own risk)",
+ )
+ parser.addini(
+ "strict_parametrization_ids",
+ type="bool",
+ # None => fallback to `strict`.
+ default=None,
+ help="Emit an error if non-unique parameter set IDs are detected",
+ )
+
+
+def pytest_generate_tests(metafunc: Metafunc) -> None:
+ for marker in metafunc.definition.iter_markers(name="parametrize"):
+ metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
+
+
+def pytest_configure(config: Config) -> None:
+ config.addinivalue_line(
+ "markers",
+ "parametrize(argnames, argvalues): call a test function multiple "
+ "times passing in different arguments in turn. argvalues generally "
+ "needs to be a list of values if argnames specifies only one name "
+ "or a list of tuples of values if argnames specifies multiple names. "
+ "Example: @parametrize('arg1', [1,2]) would lead to two calls of the "
+ "decorated test function, one with arg1=1 and another with arg1=2."
+ "see https://docs.pytest.org/en/stable/how-to/parametrize.html for more info "
+ "and examples.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "usefixtures(fixturename1, fixturename2, ...): mark tests as needing "
+ "all of the specified fixtures. see "
+ "https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures ",
+ )
+
+
+def async_fail(nodeid: str) -> None:
+ msg = (
+ "async def functions are not natively supported.\n"
+ "You need to install a suitable plugin for your async framework, for example:\n"
+ " - anyio\n"
+ " - pytest-asyncio\n"
+ " - pytest-tornasync\n"
+ " - pytest-trio\n"
+ " - pytest-twisted"
+ )
+ fail(msg, pytrace=False)
+
+
+@hookimpl(trylast=True)
+def pytest_pyfunc_call(pyfuncitem: Function) -> object | None:
+ testfunction = pyfuncitem.obj
+ if is_async_function(testfunction):
+ async_fail(pyfuncitem.nodeid)
+ funcargs = pyfuncitem.funcargs
+ testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
+ result = testfunction(**testargs)
+ if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
+ async_fail(pyfuncitem.nodeid)
+ elif result is not None:
+ warnings.warn(
+ PytestReturnNotNoneWarning(
+ f"Test functions should return None, but {pyfuncitem.nodeid} returned {type(result)!r}.\n"
+ "Did you mean to use `assert` instead of `return`?\n"
+ "See https://docs.pytest.org/en/stable/how-to/assert.html#return-not-none for more information."
+ )
+ )
+ return True
+
+
+def pytest_collect_directory(
+ path: Path, parent: nodes.Collector
+) -> nodes.Collector | None:
+ pkginit = path / "__init__.py"
+ try:
+ has_pkginit = pkginit.is_file()
+ except PermissionError:
+ # See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096.
+ return None
+ if has_pkginit:
+ return Package.from_parent(parent, path=path)
+ return None
+
+
+def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Module | None:
+ if file_path.suffix == ".py":
+ if not parent.session.isinitpath(file_path):
+ if not path_matches_patterns(
+ file_path, parent.config.getini("python_files")
+ ):
+ return None
+ ihook = parent.session.gethookproxy(file_path)
+ module: Module = ihook.pytest_pycollect_makemodule(
+ module_path=file_path, parent=parent
+ )
+ return module
+ return None
+
+
+def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
+ """Return whether path matches any of the patterns in the list of globs given."""
+ return any(fnmatch_ex(pattern, path) for pattern in patterns)
+
+
+def pytest_pycollect_makemodule(module_path: Path, parent) -> Module:
+ return Module.from_parent(parent, path=module_path)
+
+
+@hookimpl(trylast=True)
+def pytest_pycollect_makeitem(
+ collector: Module | Class, name: str, obj: object
+) -> None | nodes.Item | nodes.Collector | list[nodes.Item | nodes.Collector]:
+ assert isinstance(collector, Class | Module), type(collector)
+ # Nothing was collected elsewhere, let's do it here.
+ if safe_isclass(obj):
+ if collector.istestclass(obj, name):
+ return Class.from_parent(collector, name=name, obj=obj)
+ elif collector.istestfunction(obj, name):
+ # mock seems to store unbound methods (issue473), normalize it.
+ obj = getattr(obj, "__func__", obj)
+ # We need to try and unwrap the function if it's a functools.partial
+ # or a functools.wrapped.
+ # We mustn't if it's been wrapped with mock.patch (python 2 only).
+ if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))):
+ filename, lineno = getfslineno(obj)
+ warnings.warn_explicit(
+ message=PytestCollectionWarning(
+ f"cannot collect {name!r} because it is not a function."
+ ),
+ category=None,
+ filename=str(filename),
+ lineno=lineno + 1,
+ )
+ elif getattr(obj, "__test__", True):
+ if inspect.isgeneratorfunction(obj):
+ fail(
+ f"'yield' keyword is allowed in fixtures, but not in tests ({name})",
+ pytrace=False,
+ )
+ return list(collector._genfunctions(name, obj))
+ return None
+ return None
+
+
+class PyobjMixin(nodes.Node):
+ """this mix-in inherits from Node to carry over the typing information
+
+ as its intended to always mix in before a node
+ its position in the mro is unaffected"""
+
+ _ALLOW_MARKERS = True
+
+ @property
+ def module(self):
+ """Python module object this node was collected from (can be None)."""
+ node = self.getparent(Module)
+ return node.obj if node is not None else None
+
+ @property
+ def cls(self):
+ """Python class object this node was collected from (can be None)."""
+ node = self.getparent(Class)
+ return node.obj if node is not None else None
+
+ @property
+ def instance(self):
+ """Python instance object the function is bound to.
+
+ Returns None if not a test method, e.g. for a standalone test function,
+ a class or a module.
+ """
+ # Overridden by Function.
+ return None
+
+ @property
+ def obj(self):
+ """Underlying Python object."""
+ obj = getattr(self, "_obj", None)
+ if obj is None:
+ self._obj = obj = self._getobj()
+ # XXX evil hack
+ # used to avoid Function marker duplication
+ if self._ALLOW_MARKERS:
+ self.own_markers.extend(get_unpacked_marks(self.obj))
+ # This assumes that `obj` is called before there is a chance
+ # to add custom keys to `self.keywords`, so no fear of overriding.
+ self.keywords.update((mark.name, mark) for mark in self.own_markers)
+ return obj
+
+ @obj.setter
+ def obj(self, value):
+ self._obj = value
+
+ def _getobj(self):
+ """Get the underlying Python object. May be overwritten by subclasses."""
+ # TODO: Improve the type of `parent` such that assert/ignore aren't needed.
+ assert self.parent is not None
+ obj = self.parent.obj # type: ignore[attr-defined]
+ return getattr(obj, self.name)
+
+ def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
+ """Return Python path relative to the containing module."""
+ parts = []
+ for node in self.iter_parents():
+ name = node.name
+ if isinstance(node, Module):
+ name = os.path.splitext(name)[0]
+ if stopatmodule:
+ if includemodule:
+ parts.append(name)
+ break
+ parts.append(name)
+ parts.reverse()
+ return ".".join(parts)
+
+ def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]:
+ # XXX caching?
+ path, lineno = getfslineno(self.obj)
+ modpath = self.getmodpath()
+ return path, lineno, modpath
+
+
+# As an optimization, these builtin attribute names are pre-ignored when
+# iterating over an object during collection -- the pytest_pycollect_makeitem
+# hook is not called for them.
+# fmt: off
+class _EmptyClass: pass # noqa: E701
+IGNORED_ATTRIBUTES = frozenset.union(
+ frozenset(),
+ # Module.
+ dir(types.ModuleType("empty_module")),
+ # Some extra module attributes the above doesn't catch.
+ {"__builtins__", "__file__", "__cached__"},
+ # Class.
+ dir(_EmptyClass),
+ # Instance.
+ dir(_EmptyClass()),
+)
+del _EmptyClass
+# fmt: on
+
+
+class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
+ def funcnamefilter(self, name: str) -> bool:
+ return self._matches_prefix_or_glob_option("python_functions", name)
+
+ def isnosetest(self, obj: object) -> bool:
+ """Look for the __test__ attribute, which is applied by the
+ @nose.tools.istest decorator.
+ """
+ # We explicitly check for "is True" here to not mistakenly treat
+ # classes with a custom __getattr__ returning something truthy (like a
+ # function) as test classes.
+ return safe_getattr(obj, "__test__", False) is True
+
+ def classnamefilter(self, name: str) -> bool:
+ return self._matches_prefix_or_glob_option("python_classes", name)
+
+ def istestfunction(self, obj: object, name: str) -> bool:
+ if self.funcnamefilter(name) or self.isnosetest(obj):
+ if isinstance(obj, staticmethod | classmethod):
+ # staticmethods and classmethods need to be unwrapped.
+ obj = safe_getattr(obj, "__func__", False)
+ return callable(obj) and fixtures.getfixturemarker(obj) is None
+ else:
+ return False
+
+ def istestclass(self, obj: object, name: str) -> bool:
+ if not (self.classnamefilter(name) or self.isnosetest(obj)):
+ return False
+ if inspect.isabstract(obj):
+ return False
+ return True
+
+ def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
+ """Check if the given name matches the prefix or glob-pattern defined
+ in configuration."""
+ for option in self.config.getini(option_name):
+ if name.startswith(option):
+ return True
+ # Check that name looks like a glob-string before calling fnmatch
+ # because this is called for every name in each collected module,
+ # and fnmatch is somewhat expensive to call.
+ elif ("*" in option or "?" in option or "[" in option) and fnmatch.fnmatch(
+ name, option
+ ):
+ return True
+ return False
+
+ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
+ if not getattr(self.obj, "__test__", True):
+ return []
+
+ # Avoid random getattrs and peek in the __dict__ instead.
+ dicts = [getattr(self.obj, "__dict__", {})]
+ if isinstance(self.obj, type):
+ for basecls in self.obj.__mro__:
+ dicts.append(basecls.__dict__)
+
+ # In each class, nodes should be definition ordered.
+ # __dict__ is definition ordered.
+ seen: set[str] = set()
+ dict_values: list[list[nodes.Item | nodes.Collector]] = []
+ collect_imported_tests = self.session.config.getini("collect_imported_tests")
+ ihook = self.ihook
+ for dic in dicts:
+ values: list[nodes.Item | nodes.Collector] = []
+ # Note: seems like the dict can change during iteration -
+ # be careful not to remove the list() without consideration.
+ for name, obj in list(dic.items()):
+ if name in IGNORED_ATTRIBUTES:
+ continue
+ if name in seen:
+ continue
+ seen.add(name)
+
+ if not collect_imported_tests and isinstance(self, Module):
+ # Do not collect functions and classes from other modules.
+ if inspect.isfunction(obj) or inspect.isclass(obj):
+ if obj.__module__ != self._getobj().__name__:
+ continue
+
+ res = ihook.pytest_pycollect_makeitem(
+ collector=self, name=name, obj=obj
+ )
+ if res is None:
+ continue
+ elif isinstance(res, list):
+ values.extend(res)
+ else:
+ values.append(res)
+ dict_values.append(values)
+
+ # Between classes in the class hierarchy, reverse-MRO order -- nodes
+ # inherited from base classes should come before subclasses.
+ result = []
+ for values in reversed(dict_values):
+ result.extend(values)
+ return result
+
+ def _genfunctions(self, name: str, funcobj) -> Iterator[Function]:
+ modulecol = self.getparent(Module)
+ assert modulecol is not None
+ module = modulecol.obj
+ clscol = self.getparent(Class)
+ cls = (clscol and clscol.obj) or None
+
+ definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
+ fixtureinfo = definition._fixtureinfo
+
+ # pytest_generate_tests impls call metafunc.parametrize() which fills
+ # metafunc._calls, the outcome of the hook.
+ metafunc = Metafunc(
+ definition=definition,
+ fixtureinfo=fixtureinfo,
+ config=self.config,
+ cls=cls,
+ module=module,
+ _ispytest=True,
+ )
+ methods = []
+ if hasattr(module, "pytest_generate_tests"):
+ methods.append(module.pytest_generate_tests)
+ if cls is not None and hasattr(cls, "pytest_generate_tests"):
+ methods.append(cls().pytest_generate_tests)
+ self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
+
+ if not metafunc._calls:
+ yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
+ else:
+ metafunc._recompute_direct_params_indices()
+ # Direct parametrizations taking place in module/class-specific
+ # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure
+ # we update what the function really needs a.k.a its fixture closure. Note that
+ # direct parametrizations using `@pytest.mark.parametrize` have already been considered
+ # into making the closure using `ignore_args` arg to `getfixtureclosure`.
+ fixtureinfo.prune_dependency_tree()
+
+ for callspec in metafunc._calls:
+ subname = f"{name}[{callspec.id}]" if callspec._idlist else name
+ yield Function.from_parent(
+ self,
+ name=subname,
+ callspec=callspec,
+ fixtureinfo=fixtureinfo,
+ keywords={callspec.id: True},
+ originalname=name,
+ )
+
+
+def importtestmodule(
+ path: Path,
+ config: Config,
+):
+ # We assume we are only called once per module.
+ importmode = config.getoption("--import-mode")
+ try:
+ mod = import_path(
+ path,
+ mode=importmode,
+ root=config.rootpath,
+ consider_namespace_packages=config.getini("consider_namespace_packages"),
+ )
+ except SyntaxError as e:
+ raise nodes.Collector.CollectError(
+ ExceptionInfo.from_current().getrepr(style="short")
+ ) from e
+ except ImportPathMismatchError as e:
+ raise nodes.Collector.CollectError(
+ "import file mismatch:\n"
+ "imported module {!r} has this __file__ attribute:\n"
+ " {}\n"
+ "which is not the same as the test file we want to collect:\n"
+ " {}\n"
+ "HINT: remove __pycache__ / .pyc files and/or use a "
+ "unique basename for your test file modules".format(*e.args)
+ ) from e
+ except ImportError as e:
+ exc_info = ExceptionInfo.from_current()
+ if config.get_verbosity() < 2:
+ exc_info.traceback = exc_info.traceback.filter(filter_traceback)
+ exc_repr = (
+ exc_info.getrepr(style="short")
+ if exc_info.traceback
+ else exc_info.exconly()
+ )
+ formatted_tb = str(exc_repr)
+ raise nodes.Collector.CollectError(
+ f"ImportError while importing test module '{path}'.\n"
+ "Hint: make sure your test modules/packages have valid Python names.\n"
+ "Traceback:\n"
+ f"{formatted_tb}"
+ ) from e
+ except skip.Exception as e:
+ if e.allow_module_level:
+ raise
+ raise nodes.Collector.CollectError(
+ "Using pytest.skip outside of a test will skip the entire module. "
+ "If that's your intention, pass `allow_module_level=True`. "
+ "If you want to skip a specific test or an entire class, "
+ "use the @pytest.mark.skip or @pytest.mark.skipif decorators."
+ ) from e
+ config.pluginmanager.consider_module(mod)
+ return mod
+
+
+class Module(nodes.File, PyCollector):
+ """Collector for test classes and functions in a Python module."""
+
+ def _getobj(self):
+ return importtestmodule(self.path, self.config)
+
+ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
+ self._register_setup_module_fixture()
+ self._register_setup_function_fixture()
+ self.session._fixturemanager.parsefactories(self)
+ return super().collect()
+
+ def _register_setup_module_fixture(self) -> None:
+ """Register an autouse, module-scoped fixture for the collected module object
+ that invokes setUpModule/tearDownModule if either or both are available.
+
+ Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ setup_module = _get_first_non_fixture_func(
+ self.obj, ("setUpModule", "setup_module")
+ )
+ teardown_module = _get_first_non_fixture_func(
+ self.obj, ("tearDownModule", "teardown_module")
+ )
+
+ if setup_module is None and teardown_module is None:
+ return
+
+ def xunit_setup_module_fixture(request) -> Generator[None]:
+ module = request.module
+ if setup_module is not None:
+ _call_with_optional_argument(setup_module, module)
+ yield
+ if teardown_module is not None:
+ _call_with_optional_argument(teardown_module, module)
+
+ fixtures.register_fixture(
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
+ func=xunit_setup_module_fixture,
+ node=self,
+ scope="module",
+ autouse=True,
+ )
+
+ def _register_setup_function_fixture(self) -> None:
+ """Register an autouse, function-scoped fixture for the collected module object
+ that invokes setup_function/teardown_function if either or both are available.
+
+ Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",))
+ teardown_function = _get_first_non_fixture_func(
+ self.obj, ("teardown_function",)
+ )
+ if setup_function is None and teardown_function is None:
+ return
+
+ def xunit_setup_function_fixture(request) -> Generator[None]:
+ if request.instance is not None:
+ # in this case we are bound to an instance, so we need to let
+ # setup_method handle this
+ yield
+ return
+ function = request.function
+ if setup_function is not None:
+ _call_with_optional_argument(setup_function, function)
+ yield
+ if teardown_function is not None:
+ _call_with_optional_argument(teardown_function, function)
+
+ fixtures.register_fixture(
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
+ func=xunit_setup_function_fixture,
+ node=self,
+ scope="function",
+ autouse=True,
+ )
+
+
+class Package(nodes.Directory):
+ """Collector for files and directories in a Python packages -- directories
+ with an `__init__.py` file.
+
+ .. note::
+
+ Directories without an `__init__.py` file are instead collected by
+ :class:`~pytest.Dir` by default. Both are :class:`~pytest.Directory`
+ collectors.
+
+ .. versionchanged:: 8.0
+
+ Now inherits from :class:`~pytest.Directory`.
+ """
+
+ def __init__(
+ self,
+ fspath: None,
+ parent: nodes.Collector,
+ # NOTE: following args are unused:
+ config=None,
+ session=None,
+ nodeid=None,
+ path: Path | None = None,
+ ) -> None:
+ # NOTE: Could be just the following, but kept as-is for compat.
+ # super().__init__(self, fspath, parent=parent)
+ session = parent.session
+ super().__init__(
+ fspath=fspath,
+ path=path,
+ parent=parent,
+ config=config,
+ session=session,
+ nodeid=nodeid,
+ )
+
+ def setup(self) -> None:
+ init_mod = importtestmodule(self.path / "__init__.py", self.config)
+
+ # Not using fixtures to call setup_module here because autouse fixtures
+ # from packages are not called automatically (#4085).
+ setup_module = _get_first_non_fixture_func(
+ init_mod, ("setUpModule", "setup_module")
+ )
+ if setup_module is not None:
+ _call_with_optional_argument(setup_module, init_mod)
+
+ teardown_module = _get_first_non_fixture_func(
+ init_mod, ("tearDownModule", "teardown_module")
+ )
+ if teardown_module is not None:
+ func = partial(_call_with_optional_argument, teardown_module, init_mod)
+ self.addfinalizer(func)
+
+ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
+ # Always collect __init__.py first.
+ def sort_key(entry: os.DirEntry[str]) -> object:
+ return (entry.name != "__init__.py", entry.name)
+
+ config = self.config
+ col: nodes.Collector | None
+ cols: Sequence[nodes.Collector]
+ ihook = self.ihook
+ for direntry in scandir(self.path, sort_key):
+ if direntry.is_dir():
+ path = Path(direntry.path)
+ if not self.session.isinitpath(path, with_parents=True):
+ if ihook.pytest_ignore_collect(collection_path=path, config=config):
+ continue
+ col = ihook.pytest_collect_directory(path=path, parent=self)
+ if col is not None:
+ yield col
+
+ elif direntry.is_file():
+ path = Path(direntry.path)
+ if not self.session.isinitpath(path):
+ if ihook.pytest_ignore_collect(collection_path=path, config=config):
+ continue
+ cols = ihook.pytest_collect_file(file_path=path, parent=self)
+ yield from cols
+
+
+def _call_with_optional_argument(func, arg) -> None:
+ """Call the given function with the given argument if func accepts one argument, otherwise
+ calls func without arguments."""
+ arg_count = func.__code__.co_argcount
+ if inspect.ismethod(func):
+ arg_count -= 1
+ if arg_count:
+ func(arg)
+ else:
+ func()
+
+
+def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> object | None:
+ """Return the attribute from the given object to be used as a setup/teardown
+ xunit-style function, but only if not marked as a fixture to avoid calling it twice.
+ """
+ for name in names:
+ meth: object | None = getattr(obj, name, None)
+ if meth is not None and fixtures.getfixturemarker(meth) is None:
+ return meth
+ return None
+
+
+class Class(PyCollector):
+ """Collector for test methods (and nested classes) in a Python class."""
+
+ @classmethod
+ def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[override]
+ """The public constructor."""
+ return super().from_parent(name=name, parent=parent, **kw)
+
+ def newinstance(self):
+ return self.obj()
+
+ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
+ if not safe_getattr(self.obj, "__test__", True):
+ return []
+ if hasinit(self.obj):
+ assert self.parent is not None
+ self.warn(
+ PytestCollectionWarning(
+ f"cannot collect test class {self.obj.__name__!r} because it has a "
+ f"__init__ constructor (from: {self.parent.nodeid})"
+ )
+ )
+ return []
+ elif hasnew(self.obj):
+ assert self.parent is not None
+ self.warn(
+ PytestCollectionWarning(
+ f"cannot collect test class {self.obj.__name__!r} because it has a "
+ f"__new__ constructor (from: {self.parent.nodeid})"
+ )
+ )
+ return []
+
+ self._register_setup_class_fixture()
+ self._register_setup_method_fixture()
+
+ self.session._fixturemanager.parsefactories(
+ holder=self.newinstance(), node=self
+ )
+
+ return super().collect()
+
+ def _register_setup_class_fixture(self) -> None:
+ """Register an autouse, class scoped fixture into the collected class object
+ that invokes setup_class/teardown_class if either or both are available.
+
+ Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
+ teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
+ if setup_class is None and teardown_class is None:
+ return
+
+ def xunit_setup_class_fixture(request) -> Generator[None]:
+ cls = request.cls
+ if setup_class is not None:
+ func = getimfunc(setup_class)
+ _call_with_optional_argument(func, cls)
+ yield
+ if teardown_class is not None:
+ func = getimfunc(teardown_class)
+ _call_with_optional_argument(func, cls)
+
+ fixtures.register_fixture(
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
+ func=xunit_setup_class_fixture,
+ node=self,
+ scope="class",
+ autouse=True,
+ )
+
+ def _register_setup_method_fixture(self) -> None:
+ """Register an autouse, function scoped fixture into the collected class object
+ that invokes setup_method/teardown_method if either or both are available.
+
+ Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
+ other fixtures (#517).
+ """
+ setup_name = "setup_method"
+ setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
+ teardown_name = "teardown_method"
+ teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
+ if setup_method is None and teardown_method is None:
+ return
+
+ def xunit_setup_method_fixture(request) -> Generator[None]:
+ instance = request.instance
+ method = request.function
+ if setup_method is not None:
+ func = getattr(instance, setup_name)
+ _call_with_optional_argument(func, method)
+ yield
+ if teardown_method is not None:
+ func = getattr(instance, teardown_name)
+ _call_with_optional_argument(func, method)
+
+ fixtures.register_fixture(
+ # Use a unique name to speed up lookup.
+ name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
+ func=xunit_setup_method_fixture,
+ node=self,
+ scope="function",
+ autouse=True,
+ )
+
+
+def hasinit(obj: object) -> bool:
+ init: object = getattr(obj, "__init__", None)
+ if init:
+ return init != object.__init__
+ return False
+
+
+def hasnew(obj: object) -> bool:
+ new: object = getattr(obj, "__new__", None)
+ if new:
+ return new != object.__new__
+ return False
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class IdMaker:
+ """Make IDs for a parametrization."""
+
+ __slots__ = (
+ "argnames",
+ "config",
+ "idfn",
+ "ids",
+ "nodeid",
+ "parametersets",
+ )
+
+ # The argnames of the parametrization.
+ argnames: Sequence[str]
+ # The ParameterSets of the parametrization.
+ parametersets: Sequence[ParameterSet]
+ # Optionally, a user-provided callable to make IDs for parameters in a
+ # ParameterSet.
+ idfn: Callable[[Any], object | None] | None
+ # Optionally, explicit IDs for ParameterSets by index.
+ ids: Sequence[object | None] | None
+ # Optionally, the pytest config.
+ # Used for controlling ASCII escaping, determining parametrization ID
+ # strictness, and for calling the :hook:`pytest_make_parametrize_id` hook.
+ config: Config | None
+ # Optionally, the ID of the node being parametrized.
+ # Used only for clearer error messages.
+ nodeid: str | None
+
+ def make_unique_parameterset_ids(self) -> list[str | _HiddenParam]:
+ """Make a unique identifier for each ParameterSet, that may be used to
+ identify the parametrization in a node ID.
+
+ If strict_parametrization_ids is enabled, and duplicates are detected,
+ raises CollectError. Otherwise makes the IDs unique as follows:
+
+ Format is -...-[counter], where prm_x_token is
+ - user-provided id, if given
+ - else an id derived from the value, applicable for certain types
+ - else
+ The counter suffix is appended only in case a string wouldn't be unique
+ otherwise.
+ """
+ resolved_ids = list(self._resolve_ids())
+ # All IDs must be unique!
+ if len(resolved_ids) != len(set(resolved_ids)):
+ # Record the number of occurrences of each ID.
+ id_counts = Counter(resolved_ids)
+
+ if self._strict_parametrization_ids_enabled():
+ parameters = ", ".join(self.argnames)
+ parametersets = ", ".join(
+ [saferepr(list(param.values)) for param in self.parametersets]
+ )
+ ids = ", ".join(
+ id if id is not HIDDEN_PARAM else "" for id in resolved_ids
+ )
+ duplicates = ", ".join(
+ id if id is not HIDDEN_PARAM else ""
+ for id, count in id_counts.items()
+ if count > 1
+ )
+ msg = textwrap.dedent(f"""
+ Duplicate parametrization IDs detected, but strict_parametrization_ids is set.
+
+ Test name: {self.nodeid}
+ Parameters: {parameters}
+ Parameter sets: {parametersets}
+ IDs: {ids}
+ Duplicates: {duplicates}
+
+ You can fix this problem using `@pytest.mark.parametrize(..., ids=...)` or `pytest.param(..., id=...)`.
+ """).strip() # noqa: E501
+ raise nodes.Collector.CollectError(msg)
+
+ # Map the ID to its next suffix.
+ id_suffixes: dict[str, int] = defaultdict(int)
+ # Suffix non-unique IDs to make them unique.
+ for index, id in enumerate(resolved_ids):
+ if id_counts[id] > 1:
+ if id is HIDDEN_PARAM:
+ self._complain_multiple_hidden_parameter_sets()
+ suffix = ""
+ if id and id[-1].isdigit():
+ suffix = "_"
+ new_id = f"{id}{suffix}{id_suffixes[id]}"
+ while new_id in set(resolved_ids):
+ id_suffixes[id] += 1
+ new_id = f"{id}{suffix}{id_suffixes[id]}"
+ resolved_ids[index] = new_id
+ id_suffixes[id] += 1
+ assert len(resolved_ids) == len(set(resolved_ids)), (
+ f"Internal error: {resolved_ids=}"
+ )
+ return resolved_ids
+
+ def _strict_parametrization_ids_enabled(self) -> bool:
+ if self.config is None:
+ return False
+ strict_parametrization_ids = self.config.getini("strict_parametrization_ids")
+ if strict_parametrization_ids is None:
+ strict_parametrization_ids = self.config.getini("strict")
+ return cast(bool, strict_parametrization_ids)
+
+ def _resolve_ids(self) -> Iterable[str | _HiddenParam]:
+ """Resolve IDs for all ParameterSets (may contain duplicates)."""
+ for idx, parameterset in enumerate(self.parametersets):
+ if parameterset.id is not None:
+ # ID provided directly - pytest.param(..., id="...")
+ if parameterset.id is HIDDEN_PARAM:
+ yield HIDDEN_PARAM
+ else:
+ yield _ascii_escaped_by_config(parameterset.id, self.config)
+ elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
+ # ID provided in the IDs list - parametrize(..., ids=[...]).
+ if self.ids[idx] is HIDDEN_PARAM:
+ yield HIDDEN_PARAM
+ else:
+ yield self._idval_from_value_required(self.ids[idx], idx)
+ else:
+ # ID not provided - generate it.
+ yield "-".join(
+ self._idval(val, argname, idx)
+ for val, argname in zip(
+ parameterset.values, self.argnames, strict=True
+ )
+ )
+
+ def _idval(self, val: object, argname: str, idx: int) -> str:
+ """Make an ID for a parameter in a ParameterSet."""
+ idval = self._idval_from_function(val, argname, idx)
+ if idval is not None:
+ return idval
+ idval = self._idval_from_hook(val, argname)
+ if idval is not None:
+ return idval
+ idval = self._idval_from_value(val)
+ if idval is not None:
+ return idval
+ return self._idval_from_argname(argname, idx)
+
+ def _idval_from_function(self, val: object, argname: str, idx: int) -> str | None:
+ """Try to make an ID for a parameter in a ParameterSet using the
+ user-provided id callable, if given."""
+ if self.idfn is None:
+ return None
+ try:
+ id = self.idfn(val)
+ except Exception as e:
+ prefix = f"{self.nodeid}: " if self.nodeid is not None else ""
+ msg = "error raised while trying to determine id of parameter '{}' at position {}"
+ msg = prefix + msg.format(argname, idx)
+ raise ValueError(msg) from e
+ if id is None:
+ return None
+ return self._idval_from_value(id)
+
+ def _idval_from_hook(self, val: object, argname: str) -> str | None:
+ """Try to make an ID for a parameter in a ParameterSet by calling the
+ :hook:`pytest_make_parametrize_id` hook."""
+ if self.config:
+ id: str | None = self.config.hook.pytest_make_parametrize_id(
+ config=self.config, val=val, argname=argname
+ )
+ return id
+ return None
+
+ def _idval_from_value(self, val: object) -> str | None:
+ """Try to make an ID for a parameter in a ParameterSet from its value,
+ if the value type is supported."""
+ if isinstance(val, str | bytes):
+ return _ascii_escaped_by_config(val, self.config)
+ elif val is None or isinstance(val, float | int | bool | complex):
+ return str(val)
+ elif isinstance(val, re.Pattern):
+ return ascii_escaped(val.pattern)
+ elif val is NOTSET:
+ # Fallback to default. Note that NOTSET is an enum.Enum.
+ pass
+ elif isinstance(val, enum.Enum):
+ return str(val)
+ elif isinstance(getattr(val, "__name__", None), str):
+ # Name of a class, function, module, etc.
+ name: str = getattr(val, "__name__")
+ return name
+ return None
+
+ def _idval_from_value_required(self, val: object, idx: int) -> str:
+ """Like _idval_from_value(), but fails if the type is not supported."""
+ id = self._idval_from_value(val)
+ if id is not None:
+ return id
+
+ # Fail.
+ prefix = self._make_error_prefix()
+ msg = (
+ f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
+ "Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
+ )
+ fail(msg, pytrace=False)
+
+ @staticmethod
+ def _idval_from_argname(argname: str, idx: int) -> str:
+ """Make an ID for a parameter in a ParameterSet from the argument name
+ and the index of the ParameterSet."""
+ return str(argname) + str(idx)
+
+ def _complain_multiple_hidden_parameter_sets(self) -> NoReturn:
+ fail(
+ f"{self._make_error_prefix()}multiple instances of HIDDEN_PARAM "
+ "cannot be used in the same parametrize call, "
+ "because the tests names need to be unique."
+ )
+
+ def _make_error_prefix(self) -> str:
+ if self.nodeid is not None:
+ return f"In {self.nodeid}: "
+ else:
+ return ""
+
+
+@final
+@dataclasses.dataclass(frozen=True)
+class CallSpec2:
+ """A planned parameterized invocation of a test function.
+
+ Calculated during collection for a given test function's Metafunc.
+ Once collection is over, each callspec is turned into a single Item
+ and stored in item.callspec.
+ """
+
+ # arg name -> arg value which will be passed to a fixture of the same name.
+ params: dict[str, object] = dataclasses.field(default_factory=dict)
+ # arg name -> arg index.
+ indices: dict[str, int] = dataclasses.field(default_factory=dict)
+ # arg name -> parameter scope.
+ # Used for sorting parametrized resources.
+ _arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict)
+ # Parts which will be added to the item's name in `[..]` separated by "-".
+ _idlist: Sequence[str] = dataclasses.field(default_factory=tuple)
+ # Marks which will be applied to the item.
+ marks: list[Mark] = dataclasses.field(default_factory=list)
+
+ def setmulti(
+ self,
+ *,
+ argnames: Iterable[str],
+ valset: Iterable[object],
+ id: str | _HiddenParam,
+ marks: Iterable[Mark | MarkDecorator],
+ scope: Scope,
+ param_index: int,
+ nodeid: str,
+ ) -> CallSpec2:
+ params = self.params.copy()
+ indices = self.indices.copy()
+ arg2scope = dict(self._arg2scope)
+ for arg, val in zip(argnames, valset, strict=True):
+ if arg in params:
+ raise nodes.Collector.CollectError(
+ f"{nodeid}: duplicate parametrization of {arg!r}"
+ )
+ params[arg] = val
+ indices[arg] = param_index
+ arg2scope[arg] = scope
+ return CallSpec2(
+ params=params,
+ indices=indices,
+ _arg2scope=arg2scope,
+ _idlist=self._idlist if id is HIDDEN_PARAM else [*self._idlist, id],
+ marks=[*self.marks, *normalize_mark_list(marks)],
+ )
+
+ def getparam(self, name: str) -> object:
+ try:
+ return self.params[name]
+ except KeyError as e:
+ raise ValueError(name) from e
+
+ @property
+ def id(self) -> str:
+ return "-".join(self._idlist)
+
+
+def get_direct_param_fixture_func(request: FixtureRequest) -> Any:
+ return request.param
+
+
+class DirectParamFixtureDef(FixtureDef[FixtureValue]):
+ """A custom FixtureDef for direct parametrization fixtures.
+
+ Each parameter in direct parametrization is desugared to a parametrized
+ fixture which returns the direct parameterization value as its param.
+ We use this custom type as a "marker" for this type of FixtureDef, but
+ usually behaves like any other FixtureDef.
+ """
+
+ def __init__(self, *, node: nodes.Node, argname: str, scope: Scope) -> None:
+ super().__init__(
+ config=node.config,
+ baseid=NOTSET,
+ argname=argname,
+ func=get_direct_param_fixture_func,
+ scope=scope,
+ params=None,
+ ids=None,
+ node=node,
+ _ispytest=True,
+ )
+
+
+# Used for storing fixturedefs for direct parametrization.
+name2directparamfixturedef_key = StashKey[dict[str, DirectParamFixtureDef[object]]]()
+
+
+@final
+class Metafunc:
+ """Objects passed to the :hook:`pytest_generate_tests` hook.
+
+ They help to inspect a test function and to generate tests according to
+ test configuration or values specified in the class or module where a
+ test function is defined.
+ """
+
+ def __init__(
+ self,
+ definition: FunctionDefinition,
+ fixtureinfo: fixtures.FuncFixtureInfo,
+ config: Config,
+ cls=None,
+ module=None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+
+ #: Access to the underlying :class:`_pytest.python.FunctionDefinition`.
+ self.definition = definition
+
+ #: Access to the :class:`pytest.Config` object for the test session.
+ self.config = config
+
+ #: The module object where the test function is defined in.
+ self.module = module
+
+ #: Underlying Python test function.
+ self.function = definition.obj
+
+ #: Set of fixture names required by the test function.
+ self.fixturenames = fixtureinfo.names_closure
+
+ #: Class object where the test function is defined in or ``None``.
+ self.cls = cls
+
+ self._arg2fixturedefs = fixtureinfo.name2fixturedefs
+
+ # Result of parametrize().
+ self._calls: list[CallSpec2] = []
+
+ self._params_directness: dict[str, Literal["indirect", "direct"]] = {}
+
+ def parametrize(
+ self,
+ argnames: str | Sequence[str],
+ argvalues: Iterable[ParameterSet | Sequence[object] | object],
+ indirect: bool | Sequence[str] = False,
+ ids: Iterable[object | None] | Callable[[Any], object | None] | None = None,
+ scope: ScopeName | None = None,
+ *,
+ _param_mark: Mark | None = None,
+ ) -> None:
+ """Add new invocations to the underlying test function using the list
+ of argvalues for the given argnames. Parametrization is performed
+ during the collection phase. If you need to setup expensive resources
+ see about setting ``indirect`` to do it at test setup time instead.
+
+ Can be called multiple times per test function (but only on different
+ argument names), in which case each call parametrizes all previous
+ parametrizations, e.g.
+
+ ::
+
+ unparametrized: t
+ parametrize ["x", "y"]: t[x], t[y]
+ parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2]
+
+ :param argnames:
+ A comma-separated string denoting one or more argument names, or
+ a list/tuple of argument strings.
+
+ :param argvalues:
+ The list of argvalues determines how often a test is invoked with
+ different argument values.
+
+ If only one argname was specified argvalues is a list of values.
+ If N argnames were specified, argvalues must be a list of
+ N-tuples, where each tuple-element specifies a value for its
+ respective argname.
+
+ .. versionchanged:: 9.1
+
+ Passing a non-:class:`~collections.abc.Collection` iterable
+ (such as a generator or iterator) is deprecated. See
+ :ref:`parametrize-iterators` for details.
+
+ :param indirect:
+ A list of arguments' names (subset of argnames) or a boolean.
+ If True the list contains all names from the argnames. Each
+ argvalue corresponding to an argname in this list will
+ be passed as request.param to its respective argname fixture
+ function so that it can perform more expensive setups during the
+ setup phase of a test rather than at collection time.
+
+ :param ids:
+ Sequence of (or generator for) ids for ``argvalues``,
+ or a callable to return part of the id for each argvalue.
+
+ With sequences (and generators like ``itertools.count()``) the
+ returned ids should be of type ``string``, ``int``, ``float``,
+ ``bool``, or ``None``.
+ They are mapped to the corresponding index in ``argvalues``.
+ ``None`` means to use the auto-generated id.
+
+ .. versionadded:: 8.4
+ :ref:`hidden-param` means to hide the parameter set
+ from the test name. Can only be used at most 1 time, as
+ test names need to be unique.
+
+ If it is a callable it will be called for each entry in
+ ``argvalues``, and the return value is used as part of the
+ auto-generated id for the whole set (where parts are joined with
+ dashes ("-")).
+ This is useful to provide more specific ids for certain items, e.g.
+ dates. Returning ``None`` will use an auto-generated id.
+
+ If no ids are provided they will be generated automatically from
+ the argvalues.
+
+ :param scope:
+ If specified it denotes the scope of the parameters.
+ The scope is used for grouping tests by parameter instances.
+ It will also override any fixture-function defined scope, allowing
+ to set a dynamic scope using test context or configuration.
+ """
+ nodeid = self.definition.nodeid
+
+ argnames, parametersets = ParameterSet._for_parametrize(
+ argnames,
+ argvalues,
+ self.function,
+ self.config,
+ nodeid=self.definition.nodeid,
+ )
+ del argvalues
+
+ if "request" in argnames:
+ fail(
+ f"{nodeid}: 'request' is a reserved name and cannot be used in @pytest.mark.parametrize",
+ pytrace=False,
+ )
+
+ if scope is not None:
+ scope_ = Scope.from_user(
+ scope, descr=f"parametrize() call in {self.function.__name__}"
+ )
+ else:
+ scope_ = _find_parametrized_scope(argnames, self._arg2fixturedefs, indirect)
+
+ self._validate_if_using_arg_names(argnames, indirect)
+
+ # Use any already (possibly) generated ids with parametrize Marks.
+ if _param_mark and _param_mark._param_ids_from:
+ generated_ids = _param_mark._param_ids_from._param_ids_generated
+ if generated_ids is not None:
+ ids = generated_ids
+
+ ids = self._resolve_parameter_set_ids(
+ argnames, ids, parametersets, nodeid=self.definition.nodeid
+ )
+
+ # Store used (possibly generated) ids with parametrize Marks.
+ if _param_mark and _param_mark._param_ids_from and generated_ids is None:
+ object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
+
+ # Calculate directness.
+ arg_directness = _resolve_args_directness(
+ argnames, indirect, self.definition.nodeid
+ )
+ self._params_directness.update(arg_directness)
+
+ # Add direct parametrizations as fixturedefs to arg2fixturedefs by
+ # registering artificial DirectParamFixtureDef's such that later at test
+ # setup time we can rely on FixtureDefs to exist for all argnames.
+ node = None
+ # For scopes higher than function, a DirectParamFixtureDef might have
+ # already been created for the scope. We thus store and cache the
+ # DirectParamFixtureDef on the node related to the scope.
+ if scope_ is Scope.Function:
+ name2directparamfixturedef = None
+ else:
+ collector = self.definition.parent
+ assert collector is not None
+ node = get_scope_node(collector, scope_)
+ if node is None:
+ # If used class scope and there is no class, use module-level
+ # collector (for now).
+ if scope_ is Scope.Class:
+ assert isinstance(collector, Module)
+ node = collector
+ # If used package scope and there is no package, use session
+ # (for now).
+ elif scope_ is Scope.Package:
+ node = collector.session
+ else:
+ assert False, f"Unhandled missing scope: {scope}"
+ default: dict[str, DirectParamFixtureDef[object]] = {}
+ name2directparamfixturedef = node.stash.setdefault(
+ name2directparamfixturedef_key, default
+ )
+ for argname in argnames:
+ if arg_directness[argname] == "indirect":
+ continue
+ if (
+ name2directparamfixturedef is not None
+ and argname in name2directparamfixturedef
+ ):
+ fixturedef = name2directparamfixturedef[argname]
+ else:
+ fixturedef = DirectParamFixtureDef(
+ node=self.definition.session,
+ argname=argname,
+ scope=scope_,
+ )
+ if name2directparamfixturedef is not None:
+ name2directparamfixturedef[argname] = fixturedef
+ self._arg2fixturedefs[argname] = [fixturedef]
+
+ # Create the new calls: if we are parametrize() multiple times (by applying the decorator
+ # more than once) then we accumulate those calls generating the cartesian product
+ # of all calls.
+ newcalls = []
+ for callspec in self._calls or [CallSpec2()]:
+ for param_index, (param_id, param_set) in enumerate(
+ zip(ids, parametersets, strict=True)
+ ):
+ newcallspec = callspec.setmulti(
+ argnames=argnames,
+ valset=param_set.values,
+ id=param_id,
+ marks=param_set.marks,
+ scope=scope_,
+ param_index=param_index,
+ nodeid=nodeid,
+ )
+ newcalls.append(newcallspec)
+ self._calls = newcalls
+
+ def _resolve_parameter_set_ids(
+ self,
+ argnames: Sequence[str],
+ ids: Iterable[object | None] | Callable[[Any], object | None] | None,
+ parametersets: Sequence[ParameterSet],
+ nodeid: str,
+ ) -> list[str | _HiddenParam]:
+ """Resolve the actual ids for the given parameter sets.
+
+ :param argnames:
+ Argument names passed to ``parametrize()``.
+ :param ids:
+ The `ids` parameter of the ``parametrize()`` call (see docs).
+ :param parametersets:
+ The parameter sets, each containing a set of values corresponding
+ to ``argnames``.
+ :param nodeid str:
+ The nodeid of the definition item that generated this
+ parametrization.
+ :returns:
+ List with ids for each parameter set given.
+ """
+ if ids is None:
+ idfn = None
+ ids_ = None
+ elif callable(ids):
+ idfn = ids
+ ids_ = None
+ else:
+ idfn = None
+ ids_ = self._validate_ids(ids, parametersets)
+ id_maker = IdMaker(
+ argnames,
+ parametersets,
+ idfn,
+ ids_,
+ self.config,
+ nodeid=nodeid,
+ )
+ return id_maker.make_unique_parameterset_ids()
+
+ def _validate_ids(
+ self,
+ ids: Iterable[object | None],
+ parametersets: Sequence[ParameterSet],
+ ) -> list[object | None]:
+ try:
+ num_ids = len(ids) # type: ignore[arg-type]
+ except TypeError:
+ try:
+ iter(ids)
+ except TypeError as e:
+ raise TypeError("ids must be a callable or an iterable") from e
+ num_ids = len(parametersets)
+
+ # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
+ if num_ids != len(parametersets) and num_ids != 0:
+ nodeid = self.definition.nodeid
+ fail(
+ f"In {nodeid}: {len(parametersets)} parameter sets specified, with different number of ids: {num_ids}",
+ pytrace=False,
+ )
+
+ return list(itertools.islice(ids, num_ids))
+
+ def _validate_if_using_arg_names(
+ self,
+ argnames: Sequence[str],
+ indirect: bool | Sequence[str],
+ ) -> None:
+ """Check if all argnames are being used, by default values, or directly/indirectly.
+
+ :param List[str] argnames: List of argument names passed to ``parametrize()``.
+ :param indirect: Same as the ``indirect`` parameter of ``parametrize()``.
+ :raises ValueError: If validation fails.
+ """
+ default_arg_names = set(get_default_arg_names(self.function))
+ nodeid = self.definition.nodeid
+ for arg in argnames:
+ if arg not in self.fixturenames:
+ if arg in default_arg_names:
+ fail(
+ f"In {nodeid}: function already takes an argument '{arg}' with a default value",
+ pytrace=False,
+ )
+ else:
+ if isinstance(indirect, Sequence):
+ name = "fixture" if arg in indirect else "argument"
+ else:
+ name = "fixture" if indirect else "argument"
+ fail(
+ f"In {nodeid}: function uses no {name} '{arg}'",
+ pytrace=False,
+ )
+
+ def _recompute_direct_params_indices(self) -> None:
+ for argname, param_type in self._params_directness.items():
+ if param_type == "direct":
+ for i, callspec in enumerate(self._calls):
+ callspec.indices[argname] = i
+
+
+def _find_parametrized_scope(
+ argnames: Sequence[str],
+ arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]],
+ indirect: bool | Sequence[str],
+) -> Scope:
+ """Find the most appropriate scope for a parametrized call based on its arguments.
+
+ When there's at least one direct argument, always use "function" scope.
+
+ When a test function is parametrized and all its arguments are indirect
+ (e.g. fixtures), return the most narrow scope based on the fixtures used.
+
+ Related to issue #1832, based on code posted by @Kingdread.
+ """
+ if isinstance(indirect, Sequence):
+ all_arguments_are_fixtures = len(indirect) == len(argnames)
+ else:
+ all_arguments_are_fixtures = bool(indirect)
+
+ if all_arguments_are_fixtures:
+ fixturedefs = arg2fixturedefs or {}
+ used_scopes = [
+ fixturedef[-1]._scope
+ for name, fixturedef in fixturedefs.items()
+ if name in argnames
+ ]
+ # Takes the most narrow scope from used fixtures.
+ return min(used_scopes, default=Scope.Function)
+
+ return Scope.Function
+
+
+def _ascii_escaped_by_config(val: str | bytes, config: Config | None) -> str:
+ if config is None:
+ escape_option = False
+ else:
+ escape_option = config.getini(
+ "disable_test_id_escaping_and_forfeit_all_rights_to_community_support"
+ )
+ # TODO: If escaping is turned off and the user passes bytes,
+ # will return a bytes. For now we ignore this but the
+ # code *probably* doesn't handle this case.
+ return val if escape_option else ascii_escaped(val) # type: ignore
+
+
+class Function(PyobjMixin, nodes.Item):
+ """Item responsible for setting up and executing a Python test function.
+
+ :param name:
+ The full function name, including any decorations like those
+ added by parametrization (``my_func[my_param]``).
+ :param parent:
+ The parent Node.
+ :param config:
+ The pytest Config object.
+ :param callspec:
+ If given, this function has been parametrized and the callspec contains
+ meta information about the parametrization.
+ :param callobj:
+ If given, the object which will be called when the Function is invoked,
+ otherwise the callobj will be obtained from ``parent`` using ``originalname``.
+ :param keywords:
+ Keywords bound to the function object for "-k" matching.
+ :param session:
+ The pytest Session object.
+ :param fixtureinfo:
+ Fixture information already resolved at this fixture node..
+ :param originalname:
+ The attribute name to use for accessing the underlying function object.
+ Defaults to ``name``. Set this if name is different from the original name,
+ for example when it contains decorations like those added by parametrization
+ (``my_func[my_param]``).
+ """
+
+ # Disable since functions handle it themselves.
+ _ALLOW_MARKERS = False
+
+ def __init__(
+ self,
+ name: str,
+ parent,
+ config: Config | None = None,
+ callspec: CallSpec2 | None = None,
+ callobj=NOTSET,
+ keywords: Mapping[str, Any] | None = None,
+ session: Session | None = None,
+ fixtureinfo: FuncFixtureInfo | None = None,
+ originalname: str | None = None,
+ ) -> None:
+ super().__init__(name, parent, config=config, session=session)
+
+ if callobj is not NOTSET:
+ self._obj = callobj
+ self._instance = getattr(callobj, "__self__", None)
+
+ #: Original function name, without any decorations (for example
+ #: parametrization adds a ``"[...]"`` suffix to function names), used to access
+ #: the underlying function object from ``parent`` (in case ``callobj`` is not given
+ #: explicitly).
+ #:
+ #: .. versionadded:: 3.0
+ self.originalname = originalname or name
+
+ # Note: when FunctionDefinition is introduced, we should change ``originalname``
+ # to a readonly property that returns FunctionDefinition.name.
+
+ self.own_markers.extend(get_unpacked_marks(self.obj))
+ if callspec:
+ self.callspec = callspec
+ self.own_markers.extend(callspec.marks)
+
+ # todo: this is a hell of a hack
+ # https://github.com/pytest-dev/pytest/issues/4569
+ # Note: the order of the updates is important here; indicates what
+ # takes priority (ctor argument over function attributes over markers).
+ # Take own_markers only; NodeKeywords handles parent traversal on its own.
+ self.keywords.update((mark.name, mark) for mark in self.own_markers)
+ self.keywords.update(self.obj.__dict__)
+ if keywords:
+ self.keywords.update(keywords)
+
+ if fixtureinfo is None:
+ fm = self.session._fixturemanager
+ fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls)
+ self._fixtureinfo: FuncFixtureInfo = fixtureinfo
+ self.fixturenames = fixtureinfo.names_closure
+ self._initrequest()
+
+ # todo: determine sound type limitations
+ @classmethod
+ def from_parent(cls, parent, **kw) -> Self:
+ """The public constructor."""
+ return super().from_parent(parent=parent, **kw)
+
+ def _initrequest(self) -> None:
+ self.funcargs: dict[str, object] = {}
+ self._request = fixtures.TopRequest(self, _ispytest=True)
+
+ @property
+ def function(self):
+ """Underlying python 'function' object."""
+ return getimfunc(self.obj)
+
+ @property
+ def instance(self):
+ try:
+ return self._instance
+ except AttributeError:
+ if isinstance(self.parent, Class):
+ # Each Function gets a fresh class instance.
+ self._instance = self._getinstance()
+ else:
+ self._instance = None
+ return self._instance
+
+ def _getinstance(self):
+ if isinstance(self.parent, Class):
+ # Each Function gets a fresh class instance.
+ return self.parent.newinstance()
+ else:
+ return None
+
+ def _getobj(self):
+ instance = self.instance
+ if instance is not None:
+ parent_obj = instance
+ else:
+ assert self.parent is not None
+ parent_obj = self.parent.obj # type: ignore[attr-defined]
+ return getattr(parent_obj, self.originalname)
+
+ @property
+ def _pyfuncitem(self):
+ """(compatonly) for code expecting pytest-2.2 style request objects."""
+ return self
+
+ def runtest(self) -> None:
+ """Execute the underlying test function."""
+ self.ihook.pytest_pyfunc_call(pyfuncitem=self)
+
+ def setup(self) -> None:
+ self._request._fillfixtures()
+
+ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
+ if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
+ code = _pytest._code.Code.from_function(get_real_func(self.obj))
+ path, firstlineno = code.path, code.firstlineno
+ traceback = excinfo.traceback
+ ntraceback = traceback.cut(path=path, firstlineno=firstlineno)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.cut(path=path)
+ if ntraceback == traceback:
+ ntraceback = ntraceback.filter(filter_traceback)
+ if not ntraceback:
+ ntraceback = traceback
+ ntraceback = ntraceback.filter(excinfo)
+
+ # issue364: mark all but first and last frames to
+ # only show a single-line message for each frame.
+ if self.config.getoption("tbstyle", "auto") == "auto":
+ if len(ntraceback) > 2:
+ ntraceback = Traceback(
+ (
+ ntraceback[0],
+ *(t.with_repr_style("short") for t in ntraceback[1:-1]),
+ ntraceback[-1],
+ )
+ )
+
+ return ntraceback
+ return excinfo.traceback
+
+ # TODO: Type ignored -- breaks Liskov Substitution.
+ def repr_failure( # type: ignore[override]
+ self,
+ excinfo: ExceptionInfo[BaseException],
+ ) -> str | TerminalRepr:
+ style = self.config.getoption("tbstyle", "auto")
+ if style == "auto":
+ style = "long"
+ return self._repr_failure_py(excinfo, style=style)
+
+
+class FunctionDefinition(Function):
+ """This class is a stop gap solution until we evolve to have actual function
+ definition nodes and manage to get rid of ``metafunc``."""
+
+ def runtest(self) -> None:
+ raise RuntimeError("function definitions are not supposed to be run as tests")
+
+ setup = runtest
diff --git a/venv/lib/python3.11/site-packages/_pytest/python_api.py b/venv/lib/python3.11/site-packages/_pytest/python_api.py
new file mode 100644
index 000000000..d7f4c2dce
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/python_api.py
@@ -0,0 +1,922 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+import builtins
+from collections.abc import Collection
+from collections.abc import Mapping
+from collections.abc import Sequence
+from collections.abc import Sized
+from datetime import datetime
+from datetime import timedelta
+from decimal import Decimal
+import math
+from numbers import Complex
+import pprint
+import sys
+from typing import Any
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from numpy import ndarray
+
+
+def _compare_approx(
+ full_object: object,
+ message_data: Sequence[tuple[str, str, str]],
+ number_of_elements: int,
+ different_ids: Sequence[object],
+ max_abs_diff: float,
+ max_rel_diff: float,
+) -> list[str]:
+ message_list = list(message_data)
+ message_list.insert(0, ("Index", "Obtained", "Expected"))
+ max_sizes = [0, 0, 0]
+ for index, obtained, expected in message_list:
+ max_sizes[0] = max(max_sizes[0], len(index))
+ max_sizes[1] = max(max_sizes[1], len(obtained))
+ max_sizes[2] = max(max_sizes[2], len(expected))
+ explanation = [
+ f"comparison failed. Mismatched elements: {len(different_ids)} / {number_of_elements}:",
+ f"Max absolute difference: {max_abs_diff}",
+ f"Max relative difference: {max_rel_diff}",
+ ] + [
+ f"{indexes:<{max_sizes[0]}} | {obtained:<{max_sizes[1]}} | {expected:<{max_sizes[2]}}"
+ for indexes, obtained, expected in message_list
+ ]
+ return explanation
+
+
+# builtin pytest.approx helper
+
+
+class ApproxBase:
+ """Provide shared utilities for making approximate comparisons between
+ numbers or sequences of numbers."""
+
+ # Tell numpy to use our `__eq__` operator instead of its.
+ __array_ufunc__ = None
+ __array_priority__ = 100
+
+ def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None:
+ __tracebackhide__ = True
+ self.expected = expected
+ self.abs = abs
+ self.rel = rel
+ self.nan_ok = nan_ok
+ self._check_type()
+
+ def __repr__(self) -> str:
+ raise NotImplementedError
+
+ def _repr_compare(self, other_side: Any) -> list[str]:
+ return [
+ "comparison failed",
+ f"Obtained: {other_side}",
+ f"Expected: {self}",
+ ]
+
+ def __eq__(self, actual) -> bool:
+ return all(
+ a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual)
+ )
+
+ def __bool__(self):
+ __tracebackhide__ = True
+ raise AssertionError(
+ "approx() is not supported in a boolean context.\nDid you mean: `assert a == approx(b)`?"
+ )
+
+ # Ignore type because of https://github.com/python/mypy/issues/4266.
+ __hash__ = None # type: ignore
+
+ def __ne__(self, actual) -> bool:
+ return not (actual == self)
+
+ def _approx_scalar(self, x) -> ApproxBase:
+ if isinstance(x, Decimal):
+ return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
+ if isinstance(x, (datetime, timedelta)):
+ return ApproxTimedelta(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
+ return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
+
+ def _yield_comparisons(self, actual):
+ """Yield all the pairs of numbers to be compared.
+
+ This is used to implement the `__eq__` method.
+ """
+ raise NotImplementedError
+
+ def _check_type(self) -> None:
+ """Raise a TypeError if the expected value is not a valid type."""
+ # This is only a concern if the expected value is a sequence. In every
+ # other case, the approx() function ensures that the expected value has
+ # a numeric type. For this reason, the default is to do nothing. The
+ # classes that deal with sequences should reimplement this method to
+ # raise if there are any non-numeric elements in the sequence.
+
+
+def _recursive_sequence_map(f, x):
+ """Recursively map a function over a sequence of arbitrary depth"""
+ if isinstance(x, list | tuple):
+ seq_type = type(x)
+ return seq_type(_recursive_sequence_map(f, xi) for xi in x)
+ elif _is_sequence_like(x):
+ return [_recursive_sequence_map(f, xi) for xi in x]
+ else:
+ return f(x)
+
+
+class ApproxNumpy(ApproxBase):
+ """Perform approximate comparisons where the expected value is numpy array."""
+
+ def __repr__(self) -> str:
+ list_scalars = _recursive_sequence_map(
+ self._approx_scalar, self.expected.tolist()
+ )
+ return f"approx({list_scalars!r})"
+
+ def _repr_compare(self, other_side: ndarray | list[Any]) -> list[str]:
+ import itertools
+ import math
+
+ def get_value_from_nested_list(
+ nested_list: list[Any], nd_index: tuple[Any, ...]
+ ) -> Any:
+ """
+ Helper function to get the value out of a nested list, given an n-dimensional index.
+ This mimics numpy's indexing, but for raw nested python lists.
+ """
+ value: Any = nested_list
+ for i in nd_index:
+ value = value[i]
+ return value
+
+ np_array_shape = self.expected.shape
+ approx_side_as_seq = _recursive_sequence_map(
+ self._approx_scalar, self.expected.tolist()
+ )
+
+ # convert other_side to numpy array to ensure shape attribute is available
+ other_side_as_array = _as_numpy_array(other_side)
+ assert other_side_as_array is not None
+
+ if np_array_shape != other_side_as_array.shape:
+ return [
+ "Impossible to compare arrays with different shapes.",
+ f"Shapes: {np_array_shape} and {other_side_as_array.shape}",
+ ]
+
+ number_of_elements = self.expected.size
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for index in itertools.product(*(range(i) for i in np_array_shape)):
+ approx_value = get_value_from_nested_list(approx_side_as_seq, index)
+ other_value = get_value_from_nested_list(other_side_as_array, index)
+ if approx_value != other_value:
+ abs_diff = abs(approx_value.expected - other_value)
+ max_abs_diff = max(max_abs_diff, abs_diff)
+ if other_value == 0.0:
+ max_rel_diff = math.inf
+ else:
+ max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
+ different_ids.append(index)
+
+ message_data = [
+ (
+ str(index),
+ str(get_value_from_nested_list(other_side_as_array, index)),
+ str(get_value_from_nested_list(approx_side_as_seq, index)),
+ )
+ for index in different_ids
+ ]
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ import numpy as np
+
+ # self.expected is supposed to always be an array here.
+
+ if not np.isscalar(actual):
+ try:
+ actual = np.asarray(actual)
+ except Exception as e:
+ raise TypeError(f"cannot compare '{actual}' to numpy.ndarray") from e
+
+ if not np.isscalar(actual) and actual.shape != self.expected.shape:
+ return False
+
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ import numpy as np
+
+ # `actual` can either be a numpy array or a scalar, it is treated in
+ # `__eq__` before being passed to `ApproxBase.__eq__`, which is the
+ # only method that calls this one.
+
+ if np.isscalar(actual):
+ for i in np.ndindex(self.expected.shape):
+ yield actual, self.expected[i].item()
+ else:
+ for i in np.ndindex(self.expected.shape):
+ yield actual[i].item(), self.expected[i].item()
+
+
+class ApproxMapping(ApproxBase):
+ """Perform approximate comparisons where the expected value is a mapping
+ with numeric values (the keys can be anything)."""
+
+ def __repr__(self) -> str:
+ return f"approx({ ({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})"
+
+ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]:
+ import math
+
+ if len(self.expected) != len(other_side):
+ return [
+ "Impossible to compare mappings with different sizes.",
+ f"Lengths: {len(self.expected)} and {len(other_side)}",
+ ]
+
+ if self.expected.keys() != other_side.keys():
+ return [
+ "comparison failed.",
+ f"Mappings has different keys: expected {self.expected.keys()} but got {other_side.keys()}",
+ ]
+
+ approx_side_as_map = {
+ k: self._approx_scalar(v) for k, v in self.expected.items()
+ }
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for approx_key, approx_value in approx_side_as_map.items():
+ other_value = other_side[approx_key]
+ if approx_value != other_value:
+ if approx_value.expected is not None and other_value is not None:
+ try:
+ max_abs_diff = max(
+ max_abs_diff, abs(approx_value.expected - other_value)
+ )
+ if approx_value.expected == 0.0:
+ max_rel_diff = math.inf
+ else:
+ max_rel_diff = max(
+ max_rel_diff,
+ abs(
+ (approx_value.expected - other_value)
+ / approx_value.expected
+ ),
+ )
+ except ZeroDivisionError:
+ pass
+ different_ids.append(approx_key)
+
+ message_data = [
+ (str(key), str(other_side[key]), str(approx_side_as_map[key]))
+ for key in different_ids
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ try:
+ if set(actual.keys()) != set(self.expected.keys()):
+ return False
+ except AttributeError:
+ return False
+
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ for k in self.expected.keys():
+ yield actual[k], self.expected[k]
+
+ def _check_type(self) -> None:
+ __tracebackhide__ = True
+ for key, value in self.expected.items():
+ if isinstance(value, type(self.expected)):
+ msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
+ raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
+
+
+class ApproxSequenceLike(ApproxBase):
+ """Perform approximate comparisons where the expected value is a sequence of numbers."""
+
+ def __repr__(self) -> str:
+ seq_type = type(self.expected)
+ if seq_type not in (tuple, list):
+ seq_type = list
+ return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})"
+
+ def _repr_compare(self, other_side: Sequence[float]) -> list[str]:
+ import math
+
+ if len(self.expected) != len(other_side):
+ return [
+ "Impossible to compare lists with different sizes.",
+ f"Lengths: {len(self.expected)} and {len(other_side)}",
+ ]
+
+ approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected)
+
+ number_of_elements = len(approx_side_as_map)
+ max_abs_diff = -math.inf
+ max_rel_diff = -math.inf
+ different_ids = []
+ for i, (approx_value, other_value) in enumerate(
+ zip(approx_side_as_map, other_side, strict=True)
+ ):
+ if approx_value != other_value:
+ try:
+ abs_diff = abs(approx_value.expected - other_value)
+ max_abs_diff = max(max_abs_diff, abs_diff)
+ # Ignore non-numbers for the diff calculations (#13012).
+ except TypeError:
+ pass
+ else:
+ if other_value == 0.0:
+ max_rel_diff = math.inf
+ else:
+ max_rel_diff = max(max_rel_diff, abs_diff / abs(other_value))
+ different_ids.append(i)
+ message_data = [
+ (str(i), str(other_side[i]), str(approx_side_as_map[i]))
+ for i in different_ids
+ ]
+
+ return _compare_approx(
+ self.expected,
+ message_data,
+ number_of_elements,
+ different_ids,
+ max_abs_diff,
+ max_rel_diff,
+ )
+
+ def __eq__(self, actual) -> bool:
+ try:
+ if len(actual) != len(self.expected):
+ return False
+ except TypeError:
+ return False
+ return super().__eq__(actual)
+
+ def _yield_comparisons(self, actual):
+ return zip(actual, self.expected, strict=True)
+
+ def _check_type(self) -> None:
+ __tracebackhide__ = True
+ for index, x in enumerate(self.expected):
+ if isinstance(x, type(self.expected)):
+ msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
+ raise TypeError(msg.format(x, index, pprint.pformat(self.expected)))
+
+
+class ApproxScalar(ApproxBase):
+ """Perform approximate comparisons where the expected value is a single number."""
+
+ # Using Real should be better than this Union, but not possible yet:
+ # https://github.com/python/typeshed/pull/3108
+ DEFAULT_ABSOLUTE_TOLERANCE: float | Decimal = 1e-12
+ DEFAULT_RELATIVE_TOLERANCE: float | Decimal = 1e-6
+
+ def __repr__(self) -> str:
+ """Return a string communicating both the expected value and the
+ tolerance for the comparison being made.
+
+ For example, ``1.0 ± 1e-6``, ``(3+4j) ± 5e-6 ∠ ±180°``.
+ """
+ # Don't show a tolerance for values that aren't compared using
+ # tolerances, i.e. non-numerics and infinities. Need to call abs to
+ # handle complex numbers, e.g. (inf + 1j).
+ if (
+ isinstance(self.expected, bool)
+ or (not isinstance(self.expected, Complex | Decimal))
+ or math.isinf(abs(self.expected) or isinstance(self.expected, bool))
+ ):
+ return str(self.expected)
+
+ # If a sensible tolerance can't be calculated, self.tolerance will
+ # raise a ValueError. In this case, display '???'.
+ try:
+ if 1e-3 <= self.tolerance < 1e3:
+ vetted_tolerance = f"{self.tolerance:n}"
+ else:
+ vetted_tolerance = f"{self.tolerance:.1e}"
+
+ if (
+ isinstance(self.expected, Complex)
+ and self.expected.imag
+ and not math.isinf(self.tolerance)
+ ):
+ vetted_tolerance += " ∠ ±180°"
+ except ValueError:
+ vetted_tolerance = "???"
+
+ return f"{self.expected} ± {vetted_tolerance}"
+
+ def __eq__(self, actual) -> bool:
+ """Return whether the given value is equal to the expected value
+ within the pre-specified tolerance."""
+
+ def is_bool(val: Any) -> bool:
+ # Check if `val` is a native bool or numpy bool.
+ if isinstance(val, bool):
+ return True
+ if np := sys.modules.get("numpy"):
+ return isinstance(val, np.bool_)
+ return False
+
+ asarray = _as_numpy_array(actual)
+ if asarray is not None:
+ # Call ``__eq__()`` manually to prevent infinite-recursion with
+ # numpy<1.13. See #3748.
+ return all(self.__eq__(a) for a in asarray.flat)
+
+ # Short-circuit exact equality, except for bool and np.bool_
+ if is_bool(self.expected) and not is_bool(actual):
+ return False
+ elif actual == self.expected:
+ return True
+
+ # If either type is non-numeric, fall back to strict equality.
+ # NB: we need Complex, rather than just Number, to ensure that __abs__,
+ # __sub__, and __float__ are defined. Also, consider bool to be
+ # non-numeric, even though it has the required arithmetic.
+ if is_bool(self.expected) or not (
+ isinstance(self.expected, Complex | Decimal)
+ and isinstance(actual, Complex | Decimal)
+ ):
+ return False
+
+ # Allow the user to control whether NaNs are considered equal to each
+ # other or not. The abs() calls are for compatibility with complex
+ # numbers.
+ if math.isnan(abs(self.expected)):
+ return self.nan_ok and math.isnan(abs(actual))
+
+ # Infinity shouldn't be approximately equal to anything but itself, but
+ # if there's a relative tolerance, it will be infinite and infinity
+ # will seem approximately equal to everything. The equal-to-itself
+ # case would have been short circuited above, so here we can just
+ # return false if the expected value is infinite. The abs() call is
+ # for compatibility with complex numbers.
+ if math.isinf(abs(self.expected)):
+ return False
+
+ # Return true if the two numbers are within the tolerance.
+ result: bool = abs(self.expected - actual) <= self.tolerance
+ return result
+
+ __hash__ = None
+
+ @property
+ def tolerance(self):
+ """Return the tolerance for the comparison.
+
+ This could be either an absolute tolerance or a relative tolerance,
+ depending on what the user specified or which would be larger.
+ """
+
+ def set_default(x, default):
+ return x if x is not None else default
+
+ # Figure out what the absolute tolerance should be. ``self.abs`` is
+ # either None or a value specified by the user.
+ absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE)
+
+ if absolute_tolerance < 0:
+ raise ValueError(
+ f"absolute tolerance can't be negative: {absolute_tolerance}"
+ )
+ if math.isnan(absolute_tolerance):
+ raise ValueError("absolute tolerance can't be NaN.")
+
+ # If the user specified an absolute tolerance but not a relative one,
+ # just return the absolute tolerance.
+ if self.rel is None:
+ if self.abs is not None:
+ return absolute_tolerance
+
+ # Figure out what the relative tolerance should be. ``self.rel`` is
+ # either None or a value specified by the user. This is done after
+ # we've made sure the user didn't ask for an absolute tolerance only,
+ # because we don't want to raise errors about the relative tolerance if
+ # we aren't even going to use it.
+ relative_tolerance = set_default(
+ self.rel, self.DEFAULT_RELATIVE_TOLERANCE
+ ) * abs(self.expected)
+
+ if relative_tolerance < 0:
+ raise ValueError(
+ f"relative tolerance can't be negative: {relative_tolerance}"
+ )
+ if math.isnan(relative_tolerance):
+ raise ValueError("relative tolerance can't be NaN.")
+
+ # Return the larger of the relative and absolute tolerances.
+ return max(relative_tolerance, absolute_tolerance)
+
+
+class ApproxDecimal(ApproxScalar):
+ """Perform approximate comparisons where the expected value is a Decimal."""
+
+ DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
+ DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
+
+ def __repr__(self) -> str:
+ if isinstance(self.rel, float):
+ rel = Decimal.from_float(self.rel)
+ else:
+ rel = self.rel
+
+ if isinstance(self.abs, float):
+ abs_ = Decimal.from_float(self.abs)
+ else:
+ abs_ = self.abs
+
+ tol_str = "???"
+ if rel is not None and Decimal("1e-3") <= rel <= Decimal("1e3"):
+ tol_str = f"{rel:.1e}"
+ elif abs_ is not None:
+ tol_str = f"{abs_:.1e}"
+
+ return f"{self.expected} ± {tol_str}"
+
+
+class ApproxTimedelta(ApproxBase):
+ """Perform approximate comparisons where the expected value is a
+ datetime or timedelta.
+
+ Requires an explicit tolerance as a timedelta for abs, or a float for rel.
+ Relative tolerance is not supported for datetime comparisons.
+ """
+
+ def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None:
+ __tracebackhide__ = True
+ if isinstance(expected, datetime) and rel is not None:
+ raise TypeError(
+ "pytest.approx() does not support relative tolerance for "
+ "datetime comparisons. Use abs=timedelta(...) instead."
+ )
+ if nan_ok:
+ raise TypeError(
+ "pytest.approx() does not support nan_ok for "
+ "datetime/timedelta comparisons."
+ )
+ if abs is None and rel is None:
+ raise TypeError(
+ "pytest.approx() requires an explicit tolerance for "
+ "datetime/timedelta comparisons: "
+ "e.g. approx(expected, abs=timedelta(seconds=1)) "
+ "or approx(expected, rel=0.01)"
+ )
+ if abs is not None and not isinstance(abs, timedelta):
+ raise TypeError(
+ f"absolute tolerance for datetime/timedelta must be a "
+ f"timedelta, got {type(abs).__name__}"
+ )
+ if abs is not None and abs < timedelta(0):
+ raise ValueError(f"absolute tolerance can't be negative: {abs}")
+ if rel is not None:
+ if not isinstance(rel, (int, float)):
+ raise TypeError(
+ f"relative tolerance for timedelta must be a "
+ f"number, got {type(rel).__name__}"
+ )
+ if rel < 0:
+ raise ValueError(f"relative tolerance can't be negative: {rel}")
+ if math.isnan(rel):
+ raise ValueError("relative tolerance can't be NaN.")
+ # Compute the effective tolerance. abs_tolerance is a timedelta, rel * expected
+ # gives a timedelta (timedelta * float works in Python).
+ abs_tolerance = abs
+ rel_tolerance = rel * builtins.abs(expected) if rel is not None else None
+ if abs_tolerance is not None and rel_tolerance is not None:
+ tolerance = max(abs_tolerance, rel_tolerance)
+ else:
+ tolerance = abs_tolerance if abs_tolerance is not None else rel_tolerance
+ super().__init__(expected, rel=rel, abs=tolerance, nan_ok=False)
+
+ def __repr__(self) -> str:
+ return f"{self.expected} ± {self.abs}"
+
+ def __eq__(self, actual) -> bool:
+ try:
+ return bool(builtins.abs(self.expected - actual) <= self.abs)
+ except (TypeError, OverflowError):
+ return False
+
+ def _yield_comparisons(self, actual):
+ yield actual, self.expected
+
+ def _repr_compare(self, other_side: Any) -> list[str]:
+ try:
+ abs_diff = builtins.abs(self.expected - other_side)
+ except (TypeError, OverflowError):
+ abs_diff = "N/A"
+ return [
+ "comparison failed",
+ f"Obtained: {other_side}",
+ f"Expected: {self.expected} ± {self.abs}",
+ f"Absolute difference: {abs_diff}",
+ f"Tolerance: {self.abs}",
+ ]
+
+
+def approx(
+ expected: Any,
+ rel: float | Decimal | timedelta | None = None,
+ abs: float | Decimal | timedelta | None = None,
+ nan_ok: bool = False,
+) -> ApproxBase:
+ """Assert that two numbers (or two ordered sequences of numbers) are equal to each other
+ within some tolerance.
+
+ Due to the :doc:`python:tutorial/floatingpoint`, numbers that we
+ would intuitively expect to be equal are not always so::
+
+ >>> 0.1 + 0.2 == 0.3
+ False
+
+ This problem is commonly encountered when writing tests, e.g. when making
+ sure that floating-point values are what you expect them to be. One way to
+ deal with this problem is to assert that two floating-point numbers are
+ equal to within some appropriate tolerance::
+
+ >>> abs((0.1 + 0.2) - 0.3) < 1e-6
+ True
+
+ However, comparisons like this are tedious to write and difficult to
+ understand. Furthermore, absolute comparisons like the one above are
+ usually discouraged because there's no tolerance that works well for all
+ situations. ``1e-6`` is good for numbers around ``1``, but too small for
+ very big numbers and too big for very small ones. It's better to express
+ the tolerance as a fraction of the expected value, but relative comparisons
+ like that are even more difficult to write correctly and concisely.
+
+ The ``approx`` class performs floating-point comparisons using a syntax
+ that's as intuitive as possible::
+
+ >>> from pytest import approx
+ >>> 0.1 + 0.2 == approx(0.3)
+ True
+
+ The same syntax also works for ordered sequences of numbers::
+
+ >>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
+ True
+
+ ``numpy`` arrays::
+
+ >>> import numpy as np # doctest: +SKIP
+ >>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP
+ True
+
+ And for a ``numpy`` array against a scalar::
+
+ >>> import numpy as np # doctest: +SKIP
+ >>> np.array([0.1, 0.2]) + np.array([0.2, 0.1]) == approx(0.3) # doctest: +SKIP
+ True
+
+ Only ordered sequences are supported, because ``approx`` needs
+ to infer the relative position of the sequences without ambiguity. This means
+ ``sets`` and other unordered sequences are not supported.
+
+ Finally, dictionary *values* can also be compared::
+
+ >>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
+ True
+
+ The comparison will be true if both mappings have the same keys and their
+ respective values match the expected tolerances.
+
+ **Tolerances**
+
+ By default, ``approx`` considers numbers within a relative tolerance of
+ ``1e-6`` (i.e. one part in a million) of its expected value to be equal.
+ This treatment would lead to surprising results if the expected value was
+ ``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``.
+ To handle this case less surprisingly, ``approx`` also considers numbers
+ within an absolute tolerance of ``1e-12`` of its expected value to be
+ equal. Infinity and NaN are special cases. Infinity is only considered
+ equal to itself, regardless of the relative tolerance. NaN is not
+ considered equal to anything by default, but you can make it be equal to
+ itself by setting the ``nan_ok`` argument to True. (This is meant to
+ facilitate comparing arrays that use NaN to mean "no data".)
+
+ Both the relative and absolute tolerances can be changed by passing
+ arguments to the ``approx`` constructor::
+
+ >>> 1.0001 == approx(1)
+ False
+ >>> 1.0001 == approx(1, rel=1e-3)
+ True
+ >>> 1.0001 == approx(1, abs=1e-3)
+ True
+
+ If you specify ``abs`` but not ``rel``, the comparison will not consider
+ the relative tolerance at all. In other words, two numbers that are within
+ the default relative tolerance of ``1e-6`` will still be considered unequal
+ if they exceed the specified absolute tolerance. If you specify both
+ ``abs`` and ``rel``, the numbers will be considered equal if either
+ tolerance is met::
+
+ >>> 1 + 1e-8 == approx(1)
+ True
+ >>> 1 + 1e-8 == approx(1, abs=1e-12)
+ False
+ >>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
+ True
+
+ **Non-numeric types**
+
+ You can also use ``approx`` to compare non-numeric types, or dicts and
+ sequences containing non-numeric types, in which case it falls back to
+ strict equality. This can be useful for comparing dicts and sequences that
+ can contain optional values::
+
+ >>> {"required": 1.0000005, "optional": None} == approx({"required": 1, "optional": None})
+ True
+ >>> [None, 1.0000005] == approx([None,1])
+ True
+ >>> ["foo", 1.0000005] == approx([None,1])
+ False
+
+ **datetime and timedelta**
+
+ You can also use ``approx`` to compare :class:`~datetime.datetime` and
+ :class:`~datetime.timedelta` objects by specifying an absolute tolerance
+ as a :class:`~datetime.timedelta`::
+
+ >>> from datetime import datetime, timedelta
+ >>> dt1 = datetime(2024, 1, 1, 12, 0, 0)
+ >>> dt2 = datetime(2024, 1, 1, 12, 0, 0, 500000)
+ >>> dt1 == approx(dt2, abs=timedelta(seconds=1))
+ True
+
+ Note that ``rel`` is not supported for datetime comparisons.
+ For timedelta comparisons, ``rel`` is a number (not a timedelta) that
+ represents a relative tolerance -- a fraction of the expected value.
+ ``abs`` must be a ``timedelta`` object in both cases.
+
+ .. versionadded:: 8.4
+
+ If you're thinking about using ``approx``, then you might want to know how
+ it compares to other good ways of comparing floating-point numbers. All of
+ these algorithms are based on relative and absolute tolerances and should
+ agree for the most part, but they do have meaningful differences:
+
+ - ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative
+ tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute
+ tolerance is met. Because the relative tolerance is calculated w.r.t.
+ both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
+ ``b`` is a "reference value"). You have to specify an absolute tolerance
+ if you want to compare to ``0.0`` because there is no tolerance by
+ default. More information: :py:func:`math.isclose`.
+
+ - ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference
+ between ``a`` and ``b`` is less that the sum of the relative tolerance
+ w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance
+ is only calculated w.r.t. ``b``, this test is asymmetric and you can
+ think of ``b`` as the reference value. Support for comparing sequences
+ is provided by :py:func:`numpy.allclose`. More information:
+ :std:doc:`numpy:reference/generated/numpy.isclose`.
+
+ - ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
+ are within an absolute tolerance of ``1e-7``. No relative tolerance is
+ considered , so this function is not appropriate for very large or very
+ small numbers. Also, it's only available in subclasses of ``unittest.TestCase``
+ and it's ugly because it doesn't follow PEP8. More information:
+ :py:meth:`unittest.TestCase.assertAlmostEqual`.
+
+ - ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative
+ tolerance is met w.r.t. ``b`` or if the absolute tolerance is met.
+ Because the relative tolerance is only calculated w.r.t. ``b``, this test
+ is asymmetric and you can think of ``b`` as the reference value. In the
+ special case that you explicitly specify an absolute tolerance but not a
+ relative tolerance, only the absolute tolerance is considered.
+
+ .. note::
+
+ ``approx`` can handle numpy arrays, but we recommend the
+ specialised test helpers in :std:doc:`numpy:reference/routines.testing`
+ if you need support for comparisons, NaNs, or ULP-based tolerances.
+
+ To match strings using regex, you can use
+ `Matches `_
+ from the
+ `re_assert package `_.
+
+
+ .. note::
+
+ Unlike built-in equality, this function considers
+ booleans unequal to numeric zero or one. For example::
+
+ >>> 1 == approx(True)
+ False
+
+ .. warning::
+
+ .. versionchanged:: 3.2
+
+ In order to avoid inconsistent behavior, :py:exc:`TypeError` is
+ raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.
+ The example below illustrates the problem::
+
+ assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10)
+ assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10)
+
+ In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)``
+ to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to
+ comparison. This is because the call hierarchy of rich comparisons
+ follows a fixed behavior. More information: :py:meth:`object.__ge__`
+
+ .. versionchanged:: 3.7.1
+ ``approx`` raises ``TypeError`` when it encounters a dict value or
+ sequence element of non-numeric type.
+
+ .. versionchanged:: 6.1.0
+ ``approx`` falls back to strict equality for non-numeric types instead
+ of raising ``TypeError``.
+ """
+ # Delegate the comparison to a class that knows how to deal with the type
+ # of the expected value (e.g. int, float, list, dict, numpy.array, etc).
+ #
+ # The primary responsibility of these classes is to implement ``__eq__()``
+ # and ``__repr__()``. The former is used to actually check if some
+ # "actual" value is equivalent to the given expected value within the
+ # allowed tolerance. The latter is used to show the user the expected
+ # value and tolerance, in the case that a test failed.
+ #
+ # The actual logic for making approximate comparisons can be found in
+ # ApproxScalar, which is used to compare individual numbers. All of the
+ # other Approx classes eventually delegate to this class. The ApproxBase
+ # class provides some convenient methods and overloads, but isn't really
+ # essential.
+
+ __tracebackhide__ = True
+
+ if isinstance(expected, Decimal):
+ cls: type[ApproxBase] = ApproxDecimal
+ elif isinstance(expected, Mapping):
+ cls = ApproxMapping
+ elif (np_array := _as_numpy_array(expected)) is not None:
+ expected = np_array
+ cls = ApproxNumpy
+ elif _is_sequence_like(expected):
+ cls = ApproxSequenceLike
+ elif isinstance(expected, Collection) and not isinstance(expected, str | bytes):
+ msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
+ raise TypeError(msg)
+ elif isinstance(expected, (datetime, timedelta)):
+ cls = ApproxTimedelta
+ else:
+ cls = ApproxScalar
+
+ return cls(expected, rel, abs, nan_ok)
+
+
+def _is_sequence_like(expected: object) -> bool:
+ return (
+ hasattr(expected, "__getitem__")
+ and isinstance(expected, Sized)
+ and not isinstance(expected, str | bytes)
+ )
+
+
+def _as_numpy_array(obj: object) -> ndarray | None:
+ """
+ Return an ndarray if the given object is implicitly convertible to ndarray,
+ and numpy is already imported, otherwise None.
+ """
+ np: Any = sys.modules.get("numpy")
+ if np is not None:
+ # avoid infinite recursion on numpy scalars, which have __array__
+ if np.isscalar(obj):
+ return None
+ elif isinstance(obj, np.ndarray):
+ return obj
+ elif hasattr(obj, "__array__") or hasattr(obj, "__array_interface__"):
+ return np.asarray(obj)
+ return None
diff --git a/venv/lib/python3.11/site-packages/_pytest/raises.py b/venv/lib/python3.11/site-packages/_pytest/raises.py
new file mode 100644
index 000000000..ef91cc2c9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/raises.py
@@ -0,0 +1,1505 @@
+from __future__ import annotations
+
+from abc import ABC
+from abc import abstractmethod
+import re
+from re import Pattern
+import sys
+from textwrap import indent
+from typing import Any
+from typing import cast
+from typing import final
+from typing import Generic
+from typing import get_args
+from typing import get_origin
+from typing import Literal
+from typing import overload
+from typing import TYPE_CHECKING
+import warnings
+
+from _pytest._code import ExceptionInfo
+from _pytest._code.code import stringify_exception
+from _pytest.outcomes import fail
+from _pytest.warning_types import PytestWarning
+
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from collections.abc import Sequence
+
+ # for some reason Sphinx does not play well with 'from types import TracebackType'
+ import types
+ from typing import TypeGuard
+
+ from typing_extensions import ParamSpec
+ from typing_extensions import TypeVar
+
+ P = ParamSpec("P")
+
+ # this conditional definition is because we want to allow a TypeVar default
+ BaseExcT_co_default = TypeVar(
+ "BaseExcT_co_default",
+ bound=BaseException,
+ default=BaseException,
+ covariant=True,
+ )
+
+ # Use short name because it shows up in docs.
+ E = TypeVar("E", bound=BaseException, default=BaseException)
+else:
+ from typing import TypeVar
+
+ BaseExcT_co_default = TypeVar(
+ "BaseExcT_co_default", bound=BaseException, covariant=True
+ )
+
+# RaisesGroup doesn't work with a default.
+BaseExcT_co = TypeVar("BaseExcT_co", bound=BaseException, covariant=True)
+BaseExcT_1 = TypeVar("BaseExcT_1", bound=BaseException)
+BaseExcT_2 = TypeVar("BaseExcT_2", bound=BaseException)
+ExcT_1 = TypeVar("ExcT_1", bound=Exception)
+ExcT_2 = TypeVar("ExcT_2", bound=Exception)
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+ from exceptiongroup import ExceptionGroup
+
+
+# String patterns default to including the unicode flag.
+_REGEX_NO_FLAGS = re.compile(r"").flags
+
+
+# pytest.raises helper
+@overload
+def raises(
+ expected_exception: type[E] | tuple[type[E], ...],
+ *,
+ match: str | re.Pattern[str] | None = ...,
+ check: Callable[[E], bool] = ...,
+) -> RaisesExc[E]: ...
+
+
+@overload
+def raises(
+ *,
+ match: str | re.Pattern[str],
+ # If exception_type is not provided, check() must do any typechecks itself.
+ check: Callable[[BaseException], bool] = ...,
+) -> RaisesExc[BaseException]: ...
+
+
+@overload
+def raises(*, check: Callable[[BaseException], bool]) -> RaisesExc[BaseException]: ...
+
+
+@overload
+def raises(
+ expected_exception: type[E] | tuple[type[E], ...],
+ func: Callable[P, object],
+ *args: P.args,
+ **kwargs: P.kwargs,
+) -> ExceptionInfo[E]: ...
+
+
+def raises(
+ expected_exception: type[E] | tuple[type[E], ...] | None = None,
+ func: Callable[P, object] | None = None,
+ *args: Any,
+ **kwargs: Any,
+) -> RaisesExc[BaseException] | ExceptionInfo[E]:
+ r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
+
+ :param expected_exception:
+ The expected exception type, or a tuple if one of multiple possible
+ exception types are expected. Note that subclasses of the passed exceptions
+ will also match.
+
+ This is not a required parameter, you may opt to only use ``match`` and/or
+ ``check`` for verifying the raised exception.
+
+ :kwparam str | re.Pattern[str] | None match:
+ If specified, a string containing a regular expression,
+ or a regular expression object, that is tested against the string
+ representation of the exception and its :pep:`678` `__notes__`
+ using :func:`re.search`.
+
+ To match a literal string that may contain :ref:`special characters
+ `, the pattern can first be escaped with :func:`re.escape`.
+
+ (This is only used when ``pytest.raises`` is used as a context manager,
+ and passed through to the function otherwise.
+ When using ``pytest.raises`` as a function, you can use:
+ ``pytest.raises(Exc, func, match="passed on").match("my pattern")``.)
+
+ :kwparam Callable[[BaseException], bool] check:
+
+ .. versionadded:: 8.4
+
+ If specified, a callable that will be called with the exception as a parameter
+ after checking the type and the match regex if specified.
+ If it returns ``True`` it will be considered a match, if not it will
+ be considered a failed match.
+
+
+ Use ``pytest.raises`` as a context manager, which will capture the exception of the given
+ type, or any of its subclasses::
+
+ >>> import pytest
+ >>> with pytest.raises(ZeroDivisionError):
+ ... 1/0
+
+ If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example
+ above), or no exception at all, the check will fail instead.
+
+ You can also use the keyword argument ``match`` to assert that the
+ exception matches a text or regex::
+
+ >>> with pytest.raises(ValueError, match='must be 0 or None'):
+ ... raise ValueError("value must be 0 or None")
+
+ >>> with pytest.raises(ValueError, match=r'must be \d+$'):
+ ... raise ValueError("value must be 42")
+
+ The ``match`` argument searches the formatted exception string, which includes any
+ `PEP-678 `__ ``__notes__``:
+
+ >>> with pytest.raises(ValueError, match=r"had a note added"): # doctest: +SKIP
+ ... e = ValueError("value must be 42")
+ ... e.add_note("had a note added")
+ ... raise e
+
+ The ``check`` argument, if provided, must return True when passed the raised exception
+ for the match to be successful, otherwise an :exc:`AssertionError` is raised.
+
+ >>> import errno
+ >>> with pytest.raises(OSError, check=lambda e: e.errno == errno.EACCES):
+ ... raise OSError(errno.EACCES, "no permission to view")
+
+ The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
+ details of the captured exception::
+
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... raise ValueError("value must be 42")
+ >>> assert exc_info.type is ValueError
+ >>> assert exc_info.value.args[0] == "value must be 42"
+
+ .. warning::
+
+ Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this::
+
+ # Careful, this will catch ANY exception raised.
+ with pytest.raises(Exception):
+ some_function()
+
+ Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide
+ real bugs, where the user wrote this expecting a specific exception, but some other exception is being
+ raised due to a bug introduced during a refactoring.
+
+ Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch
+ **any** exception raised.
+
+ .. note::
+
+ When using ``pytest.raises`` as a context manager, it's worthwhile to
+ note that normal context manager rules apply and that the exception
+ raised *must* be the final line in the scope of the context manager.
+ Lines of code after that, within the scope of the context manager will
+ not be executed. For example::
+
+ >>> value = 15
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... if value > 10:
+ ... raise ValueError("value must be <= 10")
+ ... assert exc_info.type is ValueError # This will not execute.
+
+ Instead, the following approach must be taken (note the difference in
+ scope)::
+
+ >>> with pytest.raises(ValueError) as exc_info:
+ ... if value > 10:
+ ... raise ValueError("value must be <= 10")
+ ...
+ >>> assert exc_info.type is ValueError
+
+ **Expecting exception groups**
+
+ When expecting exceptions wrapped in :exc:`BaseExceptionGroup` or
+ :exc:`ExceptionGroup`, you should instead use :class:`pytest.RaisesGroup`.
+
+ **Using with** ``pytest.mark.parametrize``
+
+ When using :ref:`pytest.mark.parametrize ref`
+ it is possible to parametrize tests such that
+ some runs raise an exception and others do not.
+
+ See :ref:`parametrizing_conditional_raising` for an example.
+
+ .. seealso::
+
+ :ref:`assertraises` for more examples and detailed discussion.
+
+ .. note::
+ Similar to caught exception objects in Python, explicitly clearing
+ local references to returned ``ExceptionInfo`` objects can
+ help the Python interpreter speed up its garbage collection.
+
+ Clearing those references breaks a reference cycle
+ (``ExceptionInfo`` --> caught exception --> frame stack raising
+ the exception --> current frame stack --> local variables -->
+ ``ExceptionInfo``) which makes Python keep all objects referenced
+ from that cycle (including all local variables in the current
+ frame) alive until the next cyclic garbage collection run.
+ More detailed information can be found in the official Python
+ documentation for :ref:`the try statement `.
+ """
+ __tracebackhide__ = True
+
+ if func is None and not args:
+ if set(kwargs) - {"match", "check", "expected_exception"}:
+ msg = "Unexpected keyword arguments passed to pytest.raises: "
+ msg += ", ".join(sorted(kwargs))
+ msg += "\nUse context-manager form instead?"
+ raise TypeError(msg)
+
+ if expected_exception is None:
+ return RaisesExc(**kwargs)
+ return RaisesExc(expected_exception, **kwargs)
+
+ if not expected_exception:
+ raise ValueError(
+ f"Expected an exception type or a tuple of exception types, but got `{expected_exception!r}`. "
+ f"Raising exceptions is already understood as failing the test, so you don't need "
+ f"any special code to say 'this should never raise an exception'."
+ )
+ if not callable(func):
+ raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
+ with RaisesExc(expected_exception) as excinfo:
+ func(*args, **kwargs)
+ try:
+ return excinfo
+ finally:
+ del excinfo
+
+
+# note: RaisesExc/RaisesGroup uses fail() internally, so this alias
+# indicates (to [internal] plugins?) that `pytest.raises` will
+# raise `_pytest.outcomes.Failed`, where
+# `outcomes.Failed is outcomes.fail.Exception is raises.Exception`
+# note: this is *not* the same as `_pytest.main.Failed`
+# note: mypy does not recognize this attribute, and it's not possible
+# to use a protocol/decorator like the others in outcomes due to
+# https://github.com/python/mypy/issues/18715
+raises.Exception = fail.Exception # type: ignore[attr-defined]
+
+
+def _match_pattern(match: Pattern[str]) -> str | Pattern[str]:
+ """Helper function to remove redundant `re.compile` calls when printing regex"""
+ return match.pattern if match.flags == _REGEX_NO_FLAGS else match
+
+
+def repr_callable(fun: Callable[[BaseExcT_1], bool]) -> str:
+ """Get the repr of a ``check`` parameter.
+
+ Split out so it can be monkeypatched (e.g. by hypothesis)
+ """
+ return repr(fun)
+
+
+def backquote(s: str) -> str:
+ return "`" + s + "`"
+
+
+def _exception_type_name(
+ e: type[BaseException] | tuple[type[BaseException], ...],
+) -> str:
+ if isinstance(e, type):
+ return e.__name__
+ if len(e) == 1:
+ return e[0].__name__
+ return "(" + ", ".join(ee.__name__ for ee in e) + ")"
+
+
+def _check_raw_type(
+ expected_type: type[BaseException] | tuple[type[BaseException], ...] | None,
+ exception: BaseException,
+) -> str | None:
+ if expected_type is None or expected_type == ():
+ return None
+
+ if not isinstance(
+ exception,
+ expected_type,
+ ):
+ actual_type_str = backquote(_exception_type_name(type(exception)) + "()")
+ expected_type_str = backquote(_exception_type_name(expected_type))
+ if (
+ isinstance(exception, BaseExceptionGroup)
+ and isinstance(expected_type, type)
+ and not issubclass(expected_type, BaseExceptionGroup)
+ ):
+ return f"Unexpected nested {actual_type_str}, expected {expected_type_str}"
+ return f"{actual_type_str} is not an instance of {expected_type_str}"
+ return None
+
+
+def is_fully_escaped(s: str) -> bool:
+ # we know we won't compile with re.VERBOSE, so whitespace doesn't need to be escaped
+ metacharacters = "{}()+.*?^$[]|"
+ # Strip all escape sequences (backslash + any char), then check if any
+ # metacharacter remains unescaped in the resulting string.
+ stripped = re.sub(r"\\.", "", s)
+ return not any(c in metacharacters for c in stripped)
+
+
+def unescape(s: str) -> str:
+ return re.sub(r"\\([{}()+-.*?^$\[\]\s\\|])", r"\1", s)
+
+
+# These classes conceptually differ from ExceptionInfo in that ExceptionInfo is tied, and
+# constructed from, a particular exception - whereas these are constructed with expected
+# exceptions, and later allow matching towards particular exceptions.
+# But there's overlap in `ExceptionInfo.match` and `AbstractRaises._check_match`, as with
+# `AbstractRaises.matches` and `ExceptionInfo.errisinstance`+`ExceptionInfo.group_contains`.
+# The interaction between these classes should perhaps be improved.
+class AbstractRaises(ABC, Generic[BaseExcT_co]):
+ """ABC with common functionality shared between RaisesExc and RaisesGroup"""
+
+ def __init__(
+ self,
+ *,
+ match: str | Pattern[str] | None,
+ check: Callable[[BaseExcT_co], bool] | None,
+ ) -> None:
+ if isinstance(match, str):
+ # juggle error in order to avoid context to fail (necessary?)
+ re_error = None
+ try:
+ self.match: Pattern[str] | None = re.compile(match)
+ except re.error as e:
+ re_error = e
+ if re_error is not None:
+ fail(f"Invalid regex pattern provided to 'match': {re_error}")
+ if match == "":
+ warnings.warn(
+ PytestWarning(
+ "matching against an empty string will *always* pass. If you want "
+ "to check for an empty message you need to pass '^$'. If you don't "
+ "want to match you should pass `None` or leave out the parameter."
+ ),
+ stacklevel=2,
+ )
+ else:
+ self.match = match
+
+ # check if this is a fully escaped regex and has ^$ to match fully
+ # in which case we can do a proper diff on error
+ self.rawmatch: str | None = None
+ if isinstance(match, str) or (
+ isinstance(match, Pattern) and match.flags == _REGEX_NO_FLAGS
+ ):
+ if isinstance(match, Pattern):
+ match = match.pattern
+ if (
+ match
+ and match[0] == "^"
+ and match[-1] == "$"
+ and is_fully_escaped(match[1:-1])
+ ):
+ self.rawmatch = unescape(match[1:-1])
+
+ self.check = check
+ self._fail_reason: str | None = None
+
+ # used to suppress repeated printing of `repr(self.check)`
+ self._nested: bool = False
+
+ # set in self._parse_exc
+ self.is_baseexception = False
+
+ def _parse_exc(
+ self, exc: type[BaseExcT_1] | types.GenericAlias, expected: str
+ ) -> type[BaseExcT_1]:
+ if isinstance(exc, type) and issubclass(exc, BaseException):
+ if not issubclass(exc, Exception):
+ self.is_baseexception = True
+ return exc
+ # because RaisesGroup does not support variable number of exceptions there's
+ # still a use for RaisesExc(ExceptionGroup[Exception]).
+ origin_exc: type[BaseException] | None = get_origin(exc)
+ if origin_exc and issubclass(origin_exc, BaseExceptionGroup):
+ exc_type = get_args(exc)[0]
+ if (
+ issubclass(origin_exc, ExceptionGroup) and exc_type in (Exception, Any)
+ ) or (
+ issubclass(origin_exc, BaseExceptionGroup)
+ and exc_type in (BaseException, Any)
+ ):
+ if not issubclass(origin_exc, ExceptionGroup):
+ self.is_baseexception = True
+ return cast(type[BaseExcT_1], origin_exc)
+ else:
+ raise ValueError(
+ f"Only `ExceptionGroup[Exception]` or `BaseExceptionGroup[BaseException]` "
+ f"are accepted as generic types but got `{exc}`. "
+ f"As `raises` will catch all instances of the specified group regardless of the "
+ f"generic argument specific nested exceptions has to be checked "
+ f"with `RaisesGroup`."
+ )
+ # unclear if the Type/ValueError distinction is even helpful here
+ msg = f"Expected {expected}, but got "
+ if isinstance(exc, type): # type: ignore[unreachable]
+ raise ValueError(msg + f"{exc.__name__!r}")
+ if isinstance(exc, BaseException): # type: ignore[unreachable]
+ raise TypeError(msg + f"an exception instance: {type(exc).__name__}")
+ raise TypeError(msg + repr(type(exc).__name__))
+
+ @property
+ def fail_reason(self) -> str | None:
+ """Set after a call to :meth:`matches` to give a human-readable reason for why the match failed.
+ When used as a context manager the string will be printed as the reason for the
+ test failing."""
+ return self._fail_reason
+
+ def _check_check(
+ self: AbstractRaises[BaseExcT_1],
+ exception: BaseExcT_1,
+ ) -> bool:
+ if self.check is None:
+ return True
+
+ if self.check(exception):
+ return True
+
+ check_repr = "" if self._nested else " " + repr_callable(self.check)
+ self._fail_reason = f"check{check_repr} did not return True"
+ return False
+
+ # TODO: harmonize with ExceptionInfo.match
+ def _check_match(self, e: BaseException) -> bool:
+ if self.match is None or re.search(
+ self.match,
+ stringified_exception := stringify_exception(
+ e, include_subexception_msg=False
+ ),
+ ):
+ return True
+
+ # if we're matching a group, make sure we're explicit to reduce confusion
+ # if they're trying to match an exception contained within the group
+ maybe_specify_type = (
+ f" the `{_exception_type_name(type(e))}()`"
+ if isinstance(e, BaseExceptionGroup)
+ else ""
+ )
+ if isinstance(self.rawmatch, str):
+ from _pytest.assertion.compare_text import _diff_text
+ from _pytest.assertion.highlight import dummy_highlighter
+ from _pytest.assertion.util import _config
+ from _pytest.config import Config
+
+ verbose = (
+ _config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
+ if _config is not None
+ else 0
+ )
+ diff = list(
+ _diff_text(
+ self.rawmatch, stringified_exception, dummy_highlighter, verbose
+ )
+ )
+ self._fail_reason = ("\n" if diff[0][0] == "-" else "") + "\n".join(diff)
+ return False
+
+ self._fail_reason = (
+ f"Regex pattern did not match{maybe_specify_type}.\n"
+ f" Expected regex: {_match_pattern(self.match)!r}\n"
+ f" Actual message: {stringified_exception!r}"
+ )
+ if _match_pattern(self.match) == stringified_exception:
+ self._fail_reason += "\n Did you mean to `re.escape()` the regex?"
+ return False
+
+ @abstractmethod
+ def matches(
+ self: AbstractRaises[BaseExcT_1], exception: BaseException
+ ) -> TypeGuard[BaseExcT_1]:
+ """Check if an exception matches the requirements of this AbstractRaises.
+ If it fails, :meth:`AbstractRaises.fail_reason` should be set.
+ """
+
+
+@final
+class RaisesExc(AbstractRaises[BaseExcT_co_default]):
+ """
+ .. versionadded:: 8.4
+
+
+ This is the class constructed when calling :func:`pytest.raises`, but may be used
+ directly as a helper class with :class:`RaisesGroup` when you want to specify
+ requirements on sub-exceptions.
+
+ You don't need this if you only want to specify the type, since :class:`RaisesGroup`
+ accepts ``type[BaseException]``.
+
+ :param type[BaseException] | tuple[type[BaseException]] | None expected_exception:
+ The expected type, or one of several possible types.
+ May be ``None`` in order to only make use of ``match`` and/or ``check``
+
+ The type is checked with :func:`isinstance`, and does not need to be an exact match.
+ If that is wanted you can use the ``check`` parameter.
+
+ :kwparam str | Pattern[str] match:
+ A regex to match.
+
+ :kwparam Callable[[BaseException], bool] check:
+ If specified, a callable that will be called with the exception as a parameter
+ after checking the type and the match regex if specified.
+ If it returns ``True`` it will be considered a match, if not it will
+ be considered a failed match.
+
+ :meth:`RaisesExc.matches` can also be used standalone to check individual exceptions.
+
+ Examples::
+
+ with RaisesGroup(RaisesExc(ValueError, match="string"))
+ ...
+ with RaisesGroup(RaisesExc(check=lambda x: x.args == (3, "hello"))):
+ ...
+ with RaisesGroup(RaisesExc(check=lambda x: type(x) is ValueError)):
+ ...
+ """
+
+ # Trio bundled hypothesis monkeypatching, we will probably instead assume that
+ # hypothesis will handle that in their pytest plugin by the time this is released.
+ # Alternatively we could add a version of get_pretty_function_description ourselves
+ # https://github.com/HypothesisWorks/hypothesis/blob/8ced2f59f5c7bea3344e35d2d53e1f8f8eb9fcd8/hypothesis-python/src/hypothesis/internal/reflection.py#L439
+
+ # At least one of the three parameters must be passed.
+ @overload
+ def __init__(
+ self,
+ expected_exception: (
+ type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...]
+ ),
+ /,
+ *,
+ match: str | Pattern[str] | None = ...,
+ check: Callable[[BaseExcT_co_default], bool] | None = ...,
+ ) -> None: ...
+
+ @overload
+ def __init__(
+ self: RaisesExc[BaseException], # Give E a value.
+ /,
+ *,
+ match: str | Pattern[str] | None,
+ # If exception_type is not provided, check() must do any typechecks itself.
+ check: Callable[[BaseException], bool] | None = ...,
+ ) -> None: ...
+
+ @overload
+ def __init__(self, /, *, check: Callable[[BaseException], bool]) -> None: ...
+
+ def __init__(
+ self,
+ expected_exception: (
+ type[BaseExcT_co_default] | tuple[type[BaseExcT_co_default], ...] | None
+ ) = None,
+ /,
+ *,
+ match: str | Pattern[str] | None = None,
+ check: Callable[[BaseExcT_co_default], bool] | None = None,
+ ):
+ super().__init__(match=match, check=check)
+ if isinstance(expected_exception, tuple):
+ expected_exceptions = expected_exception
+ elif expected_exception is None:
+ expected_exceptions = ()
+ else:
+ expected_exceptions = (expected_exception,)
+
+ if (expected_exceptions == ()) and match is None and check is None:
+ raise ValueError("You must specify at least one parameter to match on.")
+
+ self.expected_exceptions = tuple(
+ self._parse_exc(e, expected="a BaseException type")
+ for e in expected_exceptions
+ )
+
+ self._just_propagate = False
+
+ def matches(
+ self,
+ exception: BaseException | None,
+ ) -> TypeGuard[BaseExcT_co_default]:
+ """Check if an exception matches the requirements of this :class:`RaisesExc`.
+ If it fails, :attr:`RaisesExc.fail_reason` will be set.
+
+ Examples::
+
+ assert RaisesExc(ValueError).matches(my_exception):
+ # is equivalent to
+ assert isinstance(my_exception, ValueError)
+
+ # this can be useful when checking e.g. the ``__cause__`` of an exception.
+ with pytest.raises(ValueError) as excinfo:
+ ...
+ assert RaisesExc(SyntaxError, match="foo").matches(excinfo.value.__cause__)
+ # above line is equivalent to
+ assert isinstance(excinfo.value.__cause__, SyntaxError)
+ assert re.search("foo", str(excinfo.value.__cause__)
+
+ """
+ self._just_propagate = False
+ if exception is None:
+ self._fail_reason = "exception is None"
+ return False
+ if not self._check_type(exception):
+ self._just_propagate = True
+ return False
+
+ if not self._check_match(exception):
+ return False
+
+ return self._check_check(exception)
+
+ def __repr__(self) -> str:
+ parameters = []
+ if self.expected_exceptions:
+ parameters.append(_exception_type_name(self.expected_exceptions))
+ if self.match is not None:
+ # If no flags were specified, discard the redundant re.compile() here.
+ parameters.append(
+ f"match={_match_pattern(self.match)!r}",
+ )
+ if self.check is not None:
+ parameters.append(f"check={repr_callable(self.check)}")
+ return f"RaisesExc({', '.join(parameters)})"
+
+ def _check_type(self, exception: BaseException) -> TypeGuard[BaseExcT_co_default]:
+ self._fail_reason = _check_raw_type(self.expected_exceptions, exception)
+ return self._fail_reason is None
+
+ def __enter__(self) -> ExceptionInfo[BaseExcT_co_default]:
+ self.excinfo: ExceptionInfo[BaseExcT_co_default] = ExceptionInfo.for_later()
+ return self.excinfo
+
+ # TODO: move common code into superclass
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: types.TracebackType | None,
+ ) -> bool:
+ __tracebackhide__ = True
+ if exc_type is None:
+ if not self.expected_exceptions:
+ fail("DID NOT RAISE any exception")
+ if len(self.expected_exceptions) == 1:
+ fail(f"DID NOT RAISE {self.expected_exceptions[0].__name__}")
+ else:
+ names = ", ".join(x.__name__ for x in self.expected_exceptions)
+ fail(f"DID NOT RAISE any of ({names})")
+
+ assert self.excinfo is not None, (
+ "Internal error - should have been constructed in __enter__"
+ )
+
+ if not self.matches(exc_val):
+ if self._just_propagate:
+ return False
+ raise AssertionError(self._fail_reason) from None
+
+ # Cast to narrow the exception type now that it's verified....
+ # even though the TypeGuard in self.matches should be narrowing
+ exc_info = cast(
+ "tuple[type[BaseExcT_co_default], BaseExcT_co_default, types.TracebackType]",
+ (exc_type, exc_val, exc_tb),
+ )
+ self.excinfo.fill_unfilled(exc_info)
+ return True
+
+
+@final
+class RaisesGroup(AbstractRaises[BaseExceptionGroup[BaseExcT_co]]):
+ """
+ .. versionadded:: 8.4
+
+ Contextmanager for checking for an expected :exc:`ExceptionGroup`.
+ This works similar to :func:`pytest.raises`, but allows for specifying the structure of an :exc:`ExceptionGroup`.
+ :meth:`ExceptionInfo.group_contains` also tries to handle exception groups,
+ but it is very bad at checking that you *didn't* get unexpected exceptions.
+
+ The catching behaviour differs from :ref:`except* `, being much
+ stricter about the structure by default.
+ By using ``allow_unwrapped=True`` and ``flatten_subgroups=True`` you can match
+ :ref:`except* ` fully when expecting a single exception.
+
+ :param args:
+ Any number of exception types, :class:`RaisesGroup` or :class:`RaisesExc`
+ to specify the exceptions contained in this exception.
+ All specified exceptions must be present in the raised group, *and no others*.
+
+ If you expect a variable number of exceptions you need to use
+ :func:`pytest.raises(ExceptionGroup) ` and manually check
+ the contained exceptions. Consider making use of :meth:`RaisesExc.matches`.
+
+ It does not care about the order of the exceptions, so
+ ``RaisesGroup(ValueError, TypeError)``
+ is equivalent to
+ ``RaisesGroup(TypeError, ValueError)``.
+ :kwparam str | re.Pattern[str] | None match:
+ If specified, a string containing a regular expression,
+ or a regular expression object, that is tested against the string
+ representation of the exception group and its :pep:`678` `__notes__`
+ using :func:`re.search`.
+
+ To match a literal string that may contain :ref:`special characters
+ `, the pattern can first be escaped with :func:`re.escape`.
+
+ Note that " (5 subgroups)" will be stripped from the ``repr`` before matching.
+ :kwparam Callable[[E], bool] check:
+ If specified, a callable that will be called with the group as a parameter
+ after successfully matching the expected exceptions. If it returns ``True``
+ it will be considered a match, if not it will be considered a failed match.
+ :kwparam bool allow_unwrapped:
+ If expecting a single exception or :class:`RaisesExc` it will match even
+ if the exception is not inside an exceptiongroup.
+
+ Using this together with ``match``, ``check`` or expecting multiple exceptions
+ will raise an error.
+ :kwparam bool flatten_subgroups:
+ "flatten" any groups inside the raised exception group, extracting all exceptions
+ inside any nested groups, before matching. Without this it expects you to
+ fully specify the nesting structure by passing :class:`RaisesGroup` as expected
+ parameter.
+
+ Examples::
+
+ with RaisesGroup(ValueError):
+ raise ExceptionGroup("", (ValueError(),))
+ # match
+ with RaisesGroup(
+ ValueError,
+ ValueError,
+ RaisesExc(TypeError, match="^expected int$"),
+ match="^my group$",
+ ):
+ raise ExceptionGroup(
+ "my group",
+ [
+ ValueError(),
+ TypeError("expected int"),
+ ValueError(),
+ ],
+ )
+ # check
+ with RaisesGroup(
+ KeyboardInterrupt,
+ match="^hello$",
+ check=lambda x: isinstance(x.__cause__, ValueError),
+ ):
+ raise BaseExceptionGroup("hello", [KeyboardInterrupt()]) from ValueError
+ # nested groups
+ with RaisesGroup(RaisesGroup(ValueError)):
+ raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
+
+ # flatten_subgroups
+ with RaisesGroup(ValueError, flatten_subgroups=True):
+ raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
+
+ # allow_unwrapped
+ with RaisesGroup(ValueError, allow_unwrapped=True):
+ raise ValueError
+
+
+ :meth:`RaisesGroup.matches` can also be used directly to check a standalone exception group.
+
+
+ The matching algorithm is greedy, which means cases such as this may fail::
+
+ with RaisesGroup(ValueError, RaisesExc(ValueError, match="hello")):
+ raise ExceptionGroup("", (ValueError("hello"), ValueError("goodbye")))
+
+ even though it generally does not care about the order of the exceptions in the group.
+ To avoid the above you should specify the first :exc:`ValueError` with a :class:`RaisesExc` as well.
+
+ .. note::
+ When raised exceptions don't match the expected ones, you'll get a detailed error
+ message explaining why. This includes ``repr(check)`` if set, which in Python can be
+ overly verbose, showing memory locations etc etc.
+
+ If installed and imported (in e.g. ``conftest.py``), the ``hypothesis`` library will
+ monkeypatch this output to provide shorter & more readable repr's.
+ """
+
+ # allow_unwrapped=True requires: singular exception, exception not being
+ # RaisesGroup instance, match is None, check is None
+ @overload
+ def __init__(
+ self,
+ expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co],
+ /,
+ *,
+ allow_unwrapped: Literal[True],
+ flatten_subgroups: bool = False,
+ ) -> None: ...
+
+ # flatten_subgroups = True also requires no nested RaisesGroup
+ @overload
+ def __init__(
+ self,
+ expected_exception: type[BaseExcT_co] | RaisesExc[BaseExcT_co],
+ /,
+ *other_exceptions: type[BaseExcT_co] | RaisesExc[BaseExcT_co],
+ flatten_subgroups: Literal[True],
+ match: str | Pattern[str] | None = None,
+ check: Callable[[BaseExceptionGroup[BaseExcT_co]], bool] | None = None,
+ ) -> None: ...
+
+ # simplify the typevars if possible (the following 3 are equivalent but go simpler->complicated)
+ # ... the first handles RaisesGroup[ValueError], the second RaisesGroup[ExceptionGroup[ValueError]],
+ # the third RaisesGroup[ValueError | ExceptionGroup[ValueError]].
+ # ... otherwise, we will get results like RaisesGroup[ValueError | ExceptionGroup[Never]] (I think)
+ # (technically correct but misleading)
+ @overload
+ def __init__(
+ self: RaisesGroup[ExcT_1],
+ expected_exception: type[ExcT_1] | RaisesExc[ExcT_1],
+ /,
+ *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1],
+ match: str | Pattern[str] | None = None,
+ check: Callable[[ExceptionGroup[ExcT_1]], bool] | None = None,
+ ) -> None: ...
+
+ @overload
+ def __init__(
+ self: RaisesGroup[ExceptionGroup[ExcT_2]],
+ expected_exception: RaisesGroup[ExcT_2],
+ /,
+ *other_exceptions: RaisesGroup[ExcT_2],
+ match: str | Pattern[str] | None = None,
+ check: Callable[[ExceptionGroup[ExceptionGroup[ExcT_2]]], bool] | None = None,
+ ) -> None: ...
+
+ @overload
+ def __init__(
+ self: RaisesGroup[ExcT_1 | ExceptionGroup[ExcT_2]],
+ expected_exception: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2],
+ /,
+ *other_exceptions: type[ExcT_1] | RaisesExc[ExcT_1] | RaisesGroup[ExcT_2],
+ match: str | Pattern[str] | None = None,
+ check: (
+ Callable[[ExceptionGroup[ExcT_1 | ExceptionGroup[ExcT_2]]], bool] | None
+ ) = None,
+ ) -> None: ...
+
+ # same as the above 3 but handling BaseException
+ @overload
+ def __init__(
+ self: RaisesGroup[BaseExcT_1],
+ expected_exception: type[BaseExcT_1] | RaisesExc[BaseExcT_1],
+ /,
+ *other_exceptions: type[BaseExcT_1] | RaisesExc[BaseExcT_1],
+ match: str | Pattern[str] | None = None,
+ check: Callable[[BaseExceptionGroup[BaseExcT_1]], bool] | None = None,
+ ) -> None: ...
+
+ @overload
+ def __init__(
+ self: RaisesGroup[BaseExceptionGroup[BaseExcT_2]],
+ expected_exception: RaisesGroup[BaseExcT_2],
+ /,
+ *other_exceptions: RaisesGroup[BaseExcT_2],
+ match: str | Pattern[str] | None = None,
+ check: (
+ Callable[[BaseExceptionGroup[BaseExceptionGroup[BaseExcT_2]]], bool] | None
+ ) = None,
+ ) -> None: ...
+
+ @overload
+ def __init__(
+ self: RaisesGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
+ expected_exception: type[BaseExcT_1]
+ | RaisesExc[BaseExcT_1]
+ | RaisesGroup[BaseExcT_2],
+ /,
+ *other_exceptions: type[BaseExcT_1]
+ | RaisesExc[BaseExcT_1]
+ | RaisesGroup[BaseExcT_2],
+ match: str | Pattern[str] | None = None,
+ check: (
+ Callable[
+ [BaseExceptionGroup[BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]]],
+ bool,
+ ]
+ | None
+ ) = None,
+ ) -> None: ...
+
+ def __init__(
+ self: RaisesGroup[ExcT_1 | BaseExcT_1 | BaseExceptionGroup[BaseExcT_2]],
+ expected_exception: type[BaseExcT_1]
+ | RaisesExc[BaseExcT_1]
+ | RaisesGroup[BaseExcT_2],
+ /,
+ *other_exceptions: type[BaseExcT_1]
+ | RaisesExc[BaseExcT_1]
+ | RaisesGroup[BaseExcT_2],
+ allow_unwrapped: bool = False,
+ flatten_subgroups: bool = False,
+ match: str | Pattern[str] | None = None,
+ check: (
+ Callable[[BaseExceptionGroup[BaseExcT_1]], bool]
+ | Callable[[ExceptionGroup[ExcT_1]], bool]
+ | None
+ ) = None,
+ ):
+ # The type hint on the `self` and `check` parameters uses different formats
+ # that are *very* hard to reconcile while adhering to the overloads, so we cast
+ # it to avoid an error when passing it to super().__init__
+ check = cast(
+ "Callable[[BaseExceptionGroup[ExcT_1|BaseExcT_1|BaseExceptionGroup[BaseExcT_2]]], bool]",
+ check,
+ )
+ super().__init__(match=match, check=check)
+ self.allow_unwrapped = allow_unwrapped
+ self.flatten_subgroups: bool = flatten_subgroups
+ self.is_baseexception = False
+
+ if allow_unwrapped and other_exceptions:
+ raise ValueError(
+ "You cannot specify multiple exceptions with `allow_unwrapped=True.`"
+ " If you want to match one of multiple possible exceptions you should"
+ " use a `RaisesExc`."
+ " E.g. `RaisesExc(check=lambda e: isinstance(e, (...)))`",
+ )
+ if allow_unwrapped and isinstance(expected_exception, RaisesGroup):
+ raise ValueError(
+ "`allow_unwrapped=True` has no effect when expecting a `RaisesGroup`."
+ " You might want it in the expected `RaisesGroup`, or"
+ " `flatten_subgroups=True` if you don't care about the structure.",
+ )
+ if allow_unwrapped and (match is not None or check is not None):
+ raise ValueError(
+ "`allow_unwrapped=True` bypasses the `match` and `check` parameters"
+ " if the exception is unwrapped. If you intended to match/check the"
+ " exception you should use a `RaisesExc` object. If you want to match/check"
+ " the exceptiongroup when the exception *is* wrapped you need to"
+ " do e.g. `if isinstance(exc.value, ExceptionGroup):"
+ " assert RaisesGroup(...).matches(exc.value)` afterwards.",
+ )
+
+ self.expected_exceptions: tuple[
+ type[BaseExcT_co] | RaisesExc[BaseExcT_co] | RaisesGroup[BaseException], ...
+ ] = tuple(
+ self._parse_excgroup(e, "a BaseException type, RaisesExc, or RaisesGroup")
+ for e in (
+ expected_exception,
+ *other_exceptions,
+ )
+ )
+
+ def _parse_excgroup(
+ self,
+ exc: (
+ type[BaseExcT_co]
+ | types.GenericAlias
+ | RaisesExc[BaseExcT_1]
+ | RaisesGroup[BaseExcT_2]
+ ),
+ expected: str,
+ ) -> type[BaseExcT_co] | RaisesExc[BaseExcT_1] | RaisesGroup[BaseExcT_2]:
+ # verify exception type and set `self.is_baseexception`
+ match exc:
+ case RaisesGroup() if self.flatten_subgroups:
+ raise ValueError(
+ "You cannot specify a nested structure inside a RaisesGroup with"
+ " `flatten_subgroups=True`. The parameter will flatten subgroups"
+ " in the raised exceptiongroup before matching, which would never"
+ " match a nested structure.",
+ )
+ case RaisesGroup() | RaisesExc():
+ self.is_baseexception |= exc.is_baseexception
+ exc._nested = True
+ return exc
+ case tuple():
+ raise TypeError(
+ f"Expected {expected}, but got {type(exc).__name__!r}.\n"
+ "RaisesGroup does not support tuples of exception types when expecting one of "
+ "several possible exception types like RaisesExc.\n"
+ "If you meant to expect a group with multiple exceptions, list them as separate arguments."
+ )
+ case _:
+ return super()._parse_exc(exc, expected)
+
+ @overload
+ def __enter__(
+ self: RaisesGroup[ExcT_1],
+ ) -> ExceptionInfo[ExceptionGroup[ExcT_1]]: ...
+ @overload
+ def __enter__(
+ self: RaisesGroup[BaseExcT_1],
+ ) -> ExceptionInfo[BaseExceptionGroup[BaseExcT_1]]: ...
+
+ def __enter__(self) -> ExceptionInfo[BaseExceptionGroup[BaseException]]:
+ self.excinfo: ExceptionInfo[BaseExceptionGroup[BaseExcT_co]] = (
+ ExceptionInfo.for_later()
+ )
+ return self.excinfo
+
+ def __repr__(self) -> str:
+ reqs = [
+ e.__name__ if isinstance(e, type) else repr(e)
+ for e in self.expected_exceptions
+ ]
+ if self.allow_unwrapped:
+ reqs.append(f"allow_unwrapped={self.allow_unwrapped}")
+ if self.flatten_subgroups:
+ reqs.append(f"flatten_subgroups={self.flatten_subgroups}")
+ if self.match is not None:
+ # If no flags were specified, discard the redundant re.compile() here.
+ reqs.append(f"match={_match_pattern(self.match)!r}")
+ if self.check is not None:
+ reqs.append(f"check={repr_callable(self.check)}")
+ return f"RaisesGroup({', '.join(reqs)})"
+
+ def _unroll_exceptions(
+ self,
+ exceptions: Sequence[BaseException],
+ ) -> Sequence[BaseException]:
+ """Used if `flatten_subgroups=True`."""
+ res: list[BaseException] = []
+ for exc in exceptions:
+ if isinstance(exc, BaseExceptionGroup):
+ res.extend(self._unroll_exceptions(exc.exceptions))
+
+ else:
+ res.append(exc)
+ return res
+
+ @overload
+ def matches(
+ self: RaisesGroup[ExcT_1],
+ exception: BaseException | None,
+ ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ...
+ @overload
+ def matches(
+ self: RaisesGroup[BaseExcT_1],
+ exception: BaseException | None,
+ ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ...
+
+ def matches(
+ self,
+ exception: BaseException | None,
+ ) -> bool:
+ """Check if an exception matches the requirements of this RaisesGroup.
+ If it fails, `RaisesGroup.fail_reason` will be set.
+
+ Example::
+
+ with pytest.raises(TypeError) as excinfo:
+ ...
+ assert RaisesGroup(ValueError).matches(excinfo.value.__cause__)
+ # the above line is equivalent to
+ myexc = excinfo.value.__cause
+ assert isinstance(myexc, BaseExceptionGroup)
+ assert len(myexc.exceptions) == 1
+ assert isinstance(myexc.exceptions[0], ValueError)
+ """
+ self._fail_reason = None
+ if exception is None:
+ self._fail_reason = "exception is None"
+ return False
+ if not isinstance(exception, BaseExceptionGroup):
+ # we opt to only print type of the exception here, as the repr would
+ # likely be quite long
+ not_group_msg = f"`{type(exception).__name__}()` is not an exception group"
+ if len(self.expected_exceptions) > 1:
+ self._fail_reason = not_group_msg
+ return False
+ # if we have 1 expected exception, check if it would work even if
+ # allow_unwrapped is not set
+ res = self._check_expected(self.expected_exceptions[0], exception)
+ if res is None and self.allow_unwrapped:
+ return True
+
+ if res is None:
+ self._fail_reason = (
+ f"{not_group_msg}, but would match with `allow_unwrapped=True`"
+ )
+ elif self.allow_unwrapped:
+ self._fail_reason = res
+ else:
+ self._fail_reason = not_group_msg
+ return False
+
+ actual_exceptions: Sequence[BaseException] = exception.exceptions
+ if self.flatten_subgroups:
+ actual_exceptions = self._unroll_exceptions(actual_exceptions)
+
+ if not self._check_match(exception):
+ self._fail_reason = cast(str, self._fail_reason)
+ old_reason = self._fail_reason
+ if (
+ len(actual_exceptions) == len(self.expected_exceptions) == 1
+ and isinstance(expected := self.expected_exceptions[0], type)
+ and isinstance(actual := actual_exceptions[0], expected)
+ and self._check_match(actual)
+ ):
+ assert self.match is not None, "can't be None if _check_match failed"
+ assert self._fail_reason is old_reason is not None
+ self._fail_reason += (
+ f"\n"
+ f" but matched the expected `{self._repr_expected(expected)}`.\n"
+ f" You might want "
+ f"`RaisesGroup(RaisesExc({expected.__name__}, match={_match_pattern(self.match)!r}))`"
+ )
+ else:
+ self._fail_reason = old_reason
+ return False
+
+ # do the full check on expected exceptions
+ if not self._check_exceptions(
+ exception,
+ actual_exceptions,
+ ):
+ self._fail_reason = cast(str, self._fail_reason)
+ assert self._fail_reason is not None
+ old_reason = self._fail_reason
+ # if we're not expecting a nested structure, and there is one, do a second
+ # pass where we try flattening it
+ if (
+ not self.flatten_subgroups
+ and not any(
+ isinstance(e, RaisesGroup) for e in self.expected_exceptions
+ )
+ and any(isinstance(e, BaseExceptionGroup) for e in actual_exceptions)
+ and self._check_exceptions(
+ exception,
+ self._unroll_exceptions(exception.exceptions),
+ )
+ ):
+ # only indent if it's a single-line reason. In a multi-line there's already
+ # indented lines that this does not belong to.
+ indent = " " if "\n" not in self._fail_reason else ""
+ self._fail_reason = (
+ old_reason
+ + f"\n{indent}Did you mean to use `flatten_subgroups=True`?"
+ )
+ else:
+ self._fail_reason = old_reason
+ return False
+
+ # Only run `self.check` once we know `exception` is of the correct type.
+ if not self._check_check(exception):
+ reason = (
+ cast(str, self._fail_reason) + f" on the {type(exception).__name__}"
+ )
+ if (
+ len(actual_exceptions) == len(self.expected_exceptions) == 1
+ and isinstance(expected := self.expected_exceptions[0], type)
+ # we explicitly break typing here :)
+ and self._check_check(actual_exceptions[0]) # type: ignore[arg-type]
+ ):
+ self._fail_reason = reason + (
+ f", but did return True for the expected {self._repr_expected(expected)}."
+ f" You might want RaisesGroup(RaisesExc({expected.__name__}, check=<...>))"
+ )
+ else:
+ self._fail_reason = reason
+ return False
+
+ return True
+
+ @staticmethod
+ def _check_expected(
+ expected_type: (
+ type[BaseException] | RaisesExc[BaseException] | RaisesGroup[BaseException]
+ ),
+ exception: BaseException,
+ ) -> str | None:
+ """Helper method for `RaisesGroup.matches` and `RaisesGroup._check_exceptions`
+ to check one of potentially several expected exceptions."""
+ if isinstance(expected_type, type):
+ return _check_raw_type(expected_type, exception)
+ res = expected_type.matches(exception)
+ if res:
+ return None
+ assert expected_type.fail_reason is not None
+ if expected_type.fail_reason.startswith("\n"):
+ return f"\n{expected_type!r}: {indent(expected_type.fail_reason, ' ')}"
+ return f"{expected_type!r}: {expected_type.fail_reason}"
+
+ @staticmethod
+ def _repr_expected(e: type[BaseException] | AbstractRaises[BaseException]) -> str:
+ """Get the repr of an expected type/RaisesExc/RaisesGroup, but we only want
+ the name if it's a type"""
+ if isinstance(e, type):
+ return _exception_type_name(e)
+ return repr(e)
+
+ @overload
+ def _check_exceptions(
+ self: RaisesGroup[ExcT_1],
+ _exception: Exception,
+ actual_exceptions: Sequence[Exception],
+ ) -> TypeGuard[ExceptionGroup[ExcT_1]]: ...
+ @overload
+ def _check_exceptions(
+ self: RaisesGroup[BaseExcT_1],
+ _exception: BaseException,
+ actual_exceptions: Sequence[BaseException],
+ ) -> TypeGuard[BaseExceptionGroup[BaseExcT_1]]: ...
+
+ def _check_exceptions(
+ self,
+ _exception: BaseException,
+ actual_exceptions: Sequence[BaseException],
+ ) -> bool:
+ """Helper method for RaisesGroup.matches that attempts to pair up expected and actual exceptions"""
+ # The _exception parameter is not used, but necessary for the TypeGuard
+
+ # full table with all results
+ results = ResultHolder(self.expected_exceptions, actual_exceptions)
+
+ # (indexes of) raised exceptions that haven't (yet) found an expected
+ remaining_actual = list(range(len(actual_exceptions)))
+ # (indexes of) expected exceptions that haven't found a matching raised
+ failed_expected: list[int] = []
+ # successful greedy matches
+ matches: dict[int, int] = {}
+
+ # loop over expected exceptions first to get a more predictable result
+ for i_exp, expected in enumerate(self.expected_exceptions):
+ for i_rem in remaining_actual:
+ res = self._check_expected(expected, actual_exceptions[i_rem])
+ results.set_result(i_exp, i_rem, res)
+ if res is None:
+ remaining_actual.remove(i_rem)
+ matches[i_exp] = i_rem
+ break
+ else:
+ failed_expected.append(i_exp)
+
+ # All exceptions matched up successfully
+ if not remaining_actual and not failed_expected:
+ return True
+
+ # in case of a single expected and single raised we simplify the output
+ if 1 == len(actual_exceptions) == len(self.expected_exceptions):
+ assert not matches
+ self._fail_reason = res
+ return False
+
+ # The test case is failing, so we can do a slow and exhaustive check to find
+ # duplicate matches etc that will be helpful in debugging
+ for i_exp, expected in enumerate(self.expected_exceptions):
+ for i_actual, actual in enumerate(actual_exceptions):
+ if results.has_result(i_exp, i_actual):
+ continue
+ results.set_result(
+ i_exp, i_actual, self._check_expected(expected, actual)
+ )
+
+ successful_str = (
+ f"{len(matches)} matched exception{'s' if len(matches) > 1 else ''}. "
+ if matches
+ else ""
+ )
+
+ # all expected were found
+ if not failed_expected and results.no_match_for_actual(remaining_actual):
+ self._fail_reason = (
+ f"{successful_str}Unexpected exception(s):"
+ f" {[actual_exceptions[i] for i in remaining_actual]!r}"
+ )
+ return False
+ # all raised exceptions were expected
+ if not remaining_actual and results.no_match_for_expected(failed_expected):
+ no_match_for_str = ", ".join(
+ self._repr_expected(self.expected_exceptions[i])
+ for i in failed_expected
+ )
+ self._fail_reason = f"{successful_str}Too few exceptions raised, found no match for: [{no_match_for_str}]"
+ return False
+
+ # if there's only one remaining and one failed, and the unmatched didn't match anything else,
+ # we elect to only print why the remaining and the failed didn't match.
+ if (
+ 1 == len(remaining_actual) == len(failed_expected)
+ and results.no_match_for_actual(remaining_actual)
+ and results.no_match_for_expected(failed_expected)
+ ):
+ self._fail_reason = f"{successful_str}{results.get_result(failed_expected[0], remaining_actual[0])}"
+ return False
+
+ # there's both expected and raised exceptions without matches
+ s = ""
+ if matches:
+ s += f"\n{successful_str}"
+ indent_1 = " " * 2
+ indent_2 = " " * 4
+
+ if not remaining_actual:
+ s += "\nToo few exceptions raised!"
+ elif not failed_expected:
+ s += "\nUnexpected exception(s)!"
+
+ if failed_expected:
+ s += "\nThe following expected exceptions did not find a match:"
+ rev_matches = {v: k for k, v in matches.items()}
+ for i_failed in failed_expected:
+ s += (
+ f"\n{indent_1}{self._repr_expected(self.expected_exceptions[i_failed])}"
+ )
+ for i_actual, actual in enumerate(actual_exceptions):
+ if results.get_result(i_failed, i_actual) is None:
+ # we print full repr of match target
+ s += (
+ f"\n{indent_2}It matches {backquote(repr(actual))} which was paired with "
+ + backquote(
+ self._repr_expected(
+ self.expected_exceptions[rev_matches[i_actual]]
+ )
+ )
+ )
+
+ if remaining_actual:
+ s += "\nThe following raised exceptions did not find a match"
+ for i_actual in remaining_actual:
+ s += f"\n{indent_1}{actual_exceptions[i_actual]!r}:"
+ for i_exp, expected in enumerate(self.expected_exceptions):
+ res = results.get_result(i_exp, i_actual)
+ if i_exp in failed_expected:
+ assert res is not None
+ if res[0] != "\n":
+ s += "\n"
+ s += indent(res, indent_2)
+ if res is None:
+ # we print full repr of match target
+ s += (
+ f"\n{indent_2}It matches {backquote(self._repr_expected(expected))} "
+ f"which was paired with {backquote(repr(actual_exceptions[matches[i_exp]]))}"
+ )
+
+ if len(self.expected_exceptions) == len(actual_exceptions) and possible_match(
+ results
+ ):
+ s += (
+ "\nThere exist a possible match when attempting an exhaustive check,"
+ " but RaisesGroup uses a greedy algorithm. "
+ "Please make your expected exceptions more stringent with `RaisesExc` etc"
+ " so the greedy algorithm can function."
+ )
+ self._fail_reason = s
+ return False
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: types.TracebackType | None,
+ ) -> bool:
+ __tracebackhide__ = True
+ if exc_type is None:
+ fail(f"DID NOT RAISE any exception, expected `{self.expected_type()}`")
+
+ assert self.excinfo is not None, (
+ "Internal error - should have been constructed in __enter__"
+ )
+
+ # group_str is the only thing that differs between RaisesExc and RaisesGroup...
+ # I might just scrap it? Or make it part of fail_reason
+ group_str = (
+ "(group)"
+ if self.allow_unwrapped and not issubclass(exc_type, BaseExceptionGroup)
+ else "group"
+ )
+
+ if not self.matches(exc_val):
+ fail(f"Raised exception {group_str} did not match: {self._fail_reason}")
+
+ # Cast to narrow the exception type now that it's verified....
+ # even though the TypeGuard in self.matches should be narrowing
+ exc_info = cast(
+ "tuple[type[BaseExceptionGroup[BaseExcT_co]], BaseExceptionGroup[BaseExcT_co], types.TracebackType]",
+ (exc_type, exc_val, exc_tb),
+ )
+ self.excinfo.fill_unfilled(exc_info)
+ return True
+
+ def expected_type(self) -> str:
+ subexcs = []
+ for e in self.expected_exceptions:
+ if isinstance(e, RaisesExc):
+ subexcs.append(repr(e))
+ elif isinstance(e, RaisesGroup):
+ subexcs.append(e.expected_type())
+ elif isinstance(e, type):
+ subexcs.append(e.__name__)
+ else: # pragma: no cover
+ raise AssertionError("unknown type")
+ group_type = "Base" if self.is_baseexception else ""
+ return f"{group_type}ExceptionGroup({', '.join(subexcs)})"
+
+
+@final
+class NotChecked:
+ """Singleton for unchecked values in ResultHolder"""
+
+
+class ResultHolder:
+ """Container for results of checking exceptions.
+ Used in RaisesGroup._check_exceptions and possible_match.
+ """
+
+ def __init__(
+ self,
+ expected_exceptions: tuple[
+ type[BaseException] | AbstractRaises[BaseException], ...
+ ],
+ actual_exceptions: Sequence[BaseException],
+ ) -> None:
+ self.results: list[list[str | type[NotChecked] | None]] = [
+ [NotChecked for _ in expected_exceptions] for _ in actual_exceptions
+ ]
+
+ def set_result(self, expected: int, actual: int, result: str | None) -> None:
+ self.results[actual][expected] = result
+
+ def get_result(self, expected: int, actual: int) -> str | None:
+ res = self.results[actual][expected]
+ assert res is not NotChecked
+ return res
+
+ def has_result(self, expected: int, actual: int) -> bool:
+ return self.results[actual][expected] is not NotChecked
+
+ def no_match_for_expected(self, expected: list[int]) -> bool:
+ for i in expected:
+ for actual_results in self.results:
+ assert actual_results[i] is not NotChecked
+ if actual_results[i] is None:
+ return False
+ return True
+
+ def no_match_for_actual(self, actual: list[int]) -> bool:
+ for i in actual:
+ for res in self.results[i]:
+ assert res is not NotChecked
+ if res is None:
+ return False
+ return True
+
+
+def possible_match(results: ResultHolder, used: set[int] | None = None) -> bool:
+ if used is None:
+ used = set()
+ curr_row = len(used)
+ if curr_row == len(results.results):
+ return True
+ return any(
+ val is None and i not in used and possible_match(results, used | {i})
+ for (i, val) in enumerate(results.results[curr_row])
+ )
diff --git a/venv/lib/python3.11/site-packages/_pytest/recwarn.py b/venv/lib/python3.11/site-packages/_pytest/recwarn.py
new file mode 100644
index 000000000..c3cb10b7f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/recwarn.py
@@ -0,0 +1,379 @@
+# mypy: allow-untyped-defs
+"""Record warnings during test function execution."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterator
+from pprint import pformat
+import re
+from types import TracebackType
+from typing import Any
+from typing import final
+from typing import overload
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+
+if TYPE_CHECKING:
+ from typing_extensions import ParamSpec
+ from typing_extensions import Self
+
+ P = ParamSpec("P")
+
+import warnings
+
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.outcomes import Exit
+from _pytest.outcomes import fail
+
+
+T = TypeVar("T")
+
+
+@fixture
+def recwarn() -> Generator[WarningsRecorder]:
+ """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
+
+ See :ref:`warnings` for information on warning categories.
+ """
+ wrec = WarningsRecorder(_ispytest=True)
+ with wrec:
+ warnings.simplefilter("default")
+ yield wrec
+
+
+@overload
+def deprecated_call(
+ *, match: str | re.Pattern[str] | None = ...
+) -> WarningsRecorder: ...
+
+
+@overload
+def deprecated_call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: ...
+
+
+def deprecated_call(
+ func: Callable[..., Any] | None = None, *args: Any, **kwargs: Any
+) -> WarningsRecorder | Any:
+ """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
+
+ This function can be used as a context manager::
+
+ >>> import warnings
+ >>> def api_call_v2():
+ ... warnings.warn('use v3 of this api', DeprecationWarning)
+ ... return 200
+
+ >>> import pytest
+ >>> with pytest.deprecated_call():
+ ... assert api_call_v2() == 200
+ >>> with pytest.deprecated_call(match="^use v3 of this api$") as warning_messages:
+ ... assert api_call_v2() == 200
+
+ You may use the keyword argument ``match`` to assert
+ that the warning matches a text or regex.
+
+ The return value is a list of :class:`warnings.WarningMessage` objects,
+ one for each warning emitted
+ (regardless of whether it is an ``expected_warning`` or not).
+ """
+ __tracebackhide__ = True
+ dep_warnings = (DeprecationWarning, PendingDeprecationWarning, FutureWarning)
+ if func is None:
+ return warns(dep_warnings, *args, **kwargs)
+
+ with warns(dep_warnings):
+ return func(*args, **kwargs)
+
+
+@overload
+def warns(
+ expected_warning: type[Warning] | tuple[type[Warning], ...] = ...,
+ *,
+ match: str | re.Pattern[str] | None = ...,
+) -> WarningsChecker: ...
+
+
+@overload
+def warns(
+ expected_warning: type[Warning] | tuple[type[Warning], ...],
+ func: Callable[P, T],
+ *args: P.args,
+ **kwargs: P.kwargs,
+) -> T: ...
+
+
+def warns(
+ expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning,
+ func: Callable[..., object] | None = None,
+ *args: Any,
+ **kwargs: Any,
+) -> WarningsChecker | Any:
+ r"""Assert that code raises a particular class of warning.
+
+ Specifically, the parameter ``expected_warning`` can be a warning class or tuple
+ of warning classes, and the code inside the ``with`` block must issue at least one
+ warning of that class or classes.
+
+ This helper produces a list of :class:`warnings.WarningMessage` objects, one for
+ each warning emitted (regardless of whether it is an ``expected_warning`` or not).
+ Since pytest 8.0, unmatched warnings are also re-emitted when the context closes.
+
+ This function should be used as a context manager::
+
+ >>> import pytest
+ >>> with pytest.warns(RuntimeWarning):
+ ... warnings.warn("my warning", RuntimeWarning)
+
+ The ``match`` keyword argument can be used to assert
+ that the warning matches a text or regex::
+
+ >>> with pytest.warns(UserWarning, match='must be 0 or None'):
+ ... warnings.warn("value must be 0 or None", UserWarning)
+
+ >>> with pytest.warns(UserWarning, match=r'must be \d+$'):
+ ... warnings.warn("value must be 42", UserWarning)
+
+ >>> with pytest.warns(UserWarning): # catch re-emitted warning
+ ... with pytest.warns(UserWarning, match=r'must be \d+$'):
+ ... warnings.warn("this is not here", UserWarning)
+ Traceback (most recent call last):
+ ...
+ Failed: Regex pattern did not match any of the 1 warnings emitted.
+ Regex: ...
+ Emitted warnings: ...UserWarning...
+
+ **Using with** ``pytest.mark.parametrize``
+
+ When using :ref:`pytest.mark.parametrize ref` it is possible to parametrize tests
+ such that some runs raise a warning and others do not.
+
+ This could be achieved in the same way as with exceptions, see
+ :ref:`parametrizing_conditional_raising` for an example.
+
+ """
+ __tracebackhide__ = True
+ if func is None and not args:
+ match: str | re.Pattern[str] | None = kwargs.pop("match", None)
+ if kwargs:
+ argnames = ", ".join(sorted(kwargs))
+ raise TypeError(
+ f"Unexpected keyword arguments passed to pytest.warns: {argnames}"
+ "\nUse context-manager form instead?"
+ )
+ return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
+ else:
+ if not callable(func):
+ raise TypeError(f"{func!r} object (type: {type(func)}) must be callable")
+ with WarningsChecker(expected_warning, _ispytest=True):
+ return func(*args, **kwargs)
+
+
+class WarningsRecorder(warnings.catch_warnings):
+ """A context manager to record raised warnings.
+
+ Each recorded warning is an instance of :class:`warnings.WarningMessage`.
+
+ Adapted from `warnings.catch_warnings`.
+
+ .. note::
+ ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated
+ differently; see :ref:`ensuring_function_triggers`.
+
+ """
+
+ def __init__(self, *, _ispytest: bool = False) -> None:
+ check_ispytest(_ispytest)
+ super().__init__(record=True)
+ self._entered = False
+ self._list: list[warnings.WarningMessage] = []
+
+ @property
+ def list(self) -> list[warnings.WarningMessage]:
+ """The list of recorded warnings."""
+ return self._list
+
+ def __getitem__(self, i: int) -> warnings.WarningMessage:
+ """Get a recorded warning by index."""
+ return self._list[i]
+
+ def __iter__(self) -> Iterator[warnings.WarningMessage]:
+ """Iterate through the recorded warnings."""
+ return iter(self._list)
+
+ def __len__(self) -> int:
+ """The number of recorded warnings."""
+ return len(self._list)
+
+ def pop(self, cls: type[Warning] = Warning) -> warnings.WarningMessage:
+ """Pop the first recorded warning which is an instance of ``cls``,
+ but not an instance of a child class of any other match.
+ Raises ``AssertionError`` if there is no match.
+ """
+ best_idx: int | None = None
+ for i, w in enumerate(self._list):
+ if w.category == cls:
+ return self._list.pop(i) # exact match, stop looking
+ if issubclass(w.category, cls) and (
+ best_idx is None
+ or not issubclass(w.category, self._list[best_idx].category)
+ ):
+ best_idx = i
+ if best_idx is not None:
+ return self._list.pop(best_idx)
+ __tracebackhide__ = True
+ raise AssertionError(f"{cls!r} not found in warning list")
+
+ def clear(self) -> None:
+ """Clear the list of recorded warnings."""
+ self._list[:] = []
+
+ # Type ignored because we basically want the `catch_warnings` generic type
+ # parameter to be ourselves but that is not possible(?).
+ def __enter__(self) -> Self: # type: ignore[override]
+ if self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError(f"Cannot enter {self!r} twice")
+ _list = super().__enter__()
+ # record=True means it's None.
+ assert _list is not None
+ self._list = _list
+ warnings.simplefilter("always")
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ if not self._entered:
+ __tracebackhide__ = True
+ raise RuntimeError(f"Cannot exit {self!r} without entering first")
+
+ super().__exit__(exc_type, exc_val, exc_tb)
+
+ # Built-in catch_warnings does not reset entered state so we do it
+ # manually here for this context manager to become reusable.
+ self._entered = False
+
+
+@final
+class WarningsChecker(WarningsRecorder):
+ def __init__(
+ self,
+ expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning,
+ match_expr: str | re.Pattern[str] | None = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ super().__init__(_ispytest=True)
+
+ msg = "exceptions must be derived from Warning, not %s"
+ if isinstance(expected_warning, tuple):
+ for exc in expected_warning:
+ if not issubclass(exc, Warning):
+ raise TypeError(msg % type(exc))
+ expected_warning_tup = expected_warning
+ elif isinstance(expected_warning, type) and issubclass(
+ expected_warning, Warning
+ ):
+ expected_warning_tup = (expected_warning,)
+ else:
+ raise TypeError(msg % type(expected_warning))
+
+ self.expected_warning = expected_warning_tup
+ self.match_expr = match_expr
+
+ def matches(self, warning: warnings.WarningMessage) -> bool:
+ assert self.expected_warning is not None
+ return issubclass(warning.category, self.expected_warning) and bool(
+ self.match_expr is None or re.search(self.match_expr, str(warning.message))
+ )
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ super().__exit__(exc_type, exc_val, exc_tb)
+
+ __tracebackhide__ = True
+
+ # BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within
+ # pytest.warns should *not* trigger "DID NOT WARN" and get suppressed
+ # when the warning doesn't happen. Control-flow exceptions should always
+ # propagate.
+ if exc_val is not None and (
+ not isinstance(exc_val, Exception)
+ # Exit is an Exception, not a BaseException, for some reason.
+ or isinstance(exc_val, Exit)
+ ):
+ return
+
+ def found_str() -> str:
+ return pformat([record.message for record in self], indent=2)
+
+ try:
+ if not any(issubclass(w.category, self.expected_warning) for w in self):
+ fail(
+ f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n"
+ f" Emitted warnings: {found_str()}."
+ )
+ elif not any(self.matches(w) for w in self):
+ escape_hint = ""
+ if isinstance(self.match_expr, str) and any(
+ self.match_expr == str(w.message)
+ for w in self
+ if issubclass(w.category, self.expected_warning)
+ ):
+ escape_hint = "\n Did you mean to `re.escape()` the regex?"
+ fail(
+ f"Regex pattern did not match any of the {len(self)} warnings emitted.\n"
+ f" Regex: {self.match_expr!r}\n"
+ f" Emitted warnings: {found_str()}.{escape_hint}"
+ )
+ finally:
+ # Whether or not any warnings matched, we want to re-emit all unmatched warnings.
+ for w in self:
+ if not self.matches(w):
+ warnings.warn_explicit(
+ message=w.message,
+ category=w.category,
+ filename=w.filename,
+ lineno=w.lineno,
+ module=w.__module__,
+ source=w.source,
+ )
+
+ # Currently in Python it is possible to pass other types than an
+ # `str` message when creating `Warning` instances, however this
+ # causes an exception when :func:`warnings.filterwarnings` is used
+ # to filter those warnings. See
+ # https://github.com/python/cpython/issues/103577 for a discussion.
+ # While this can be considered a bug in CPython, we put guards in
+ # pytest as the error message produced without this check in place
+ # is confusing (#10865).
+ for w in self:
+ if type(w.message) is not UserWarning:
+ # If the warning was of an incorrect type then `warnings.warn()`
+ # creates a UserWarning. Any other warning must have been specified
+ # explicitly.
+ continue
+ if not w.message.args:
+ # UserWarning() without arguments must have been specified explicitly.
+ continue
+ msg = w.message.args[0]
+ if isinstance(msg, str):
+ continue
+ # It's possible that UserWarning was explicitly specified, and
+ # its first argument was not a string. But that case can't be
+ # distinguished from an invalid type.
+ raise TypeError(
+ f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})"
+ )
diff --git a/venv/lib/python3.11/site-packages/_pytest/reports.py b/venv/lib/python3.11/site-packages/_pytest/reports.py
new file mode 100644
index 000000000..011a69db0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/reports.py
@@ -0,0 +1,694 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+from collections.abc import Iterable
+from collections.abc import Iterator
+from collections.abc import Mapping
+from collections.abc import Sequence
+import dataclasses
+from io import StringIO
+import os
+from pprint import pprint
+import sys
+from typing import Any
+from typing import cast
+from typing import final
+from typing import Literal
+from typing import NoReturn
+from typing import TYPE_CHECKING
+
+from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import ExceptionRepr
+from _pytest._code.code import ReprEntry
+from _pytest._code.code import ReprEntryNative
+from _pytest._code.code import ReprExceptionInfo
+from _pytest._code.code import ReprFileLocation
+from _pytest._code.code import ReprFuncArgs
+from _pytest._code.code import ReprLocals
+from _pytest._code.code import ReprTraceback
+from _pytest._code.code import TerminalRepr
+from _pytest._io import TerminalWriter
+from _pytest.config import Config
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+ from _pytest.runner import CallInfo
+
+
+def getworkerinfoline(node):
+ try:
+ return node._workerinfocache
+ except AttributeError:
+ d = node.workerinfo
+ ver = "{}.{}.{}".format(*d["version_info"][:3])
+ node._workerinfocache = s = "[{}] {} -- Python {} {}".format(
+ d["id"], d["sysplatform"], ver, d["executable"]
+ )
+ return s
+
+
+class BaseReport:
+ when: str | None
+ location: tuple[str, int | None, str] | None
+ longrepr: (
+ None | ExceptionInfo[BaseException] | tuple[str, int, str] | str | TerminalRepr
+ )
+ sections: list[tuple[str, str]]
+ nodeid: str
+ outcome: Literal["passed", "failed", "skipped"]
+
+ def __init__(self, **kw: Any) -> None:
+ self.__dict__.update(kw)
+
+ if TYPE_CHECKING:
+ # Can have arbitrary fields given to __init__().
+ def __getattr__(self, key: str) -> Any: ...
+
+ def toterminal(self, out: TerminalWriter) -> None:
+ if hasattr(self, "node"):
+ worker_info = getworkerinfoline(self.node)
+ if worker_info:
+ out.line(worker_info)
+
+ longrepr = self.longrepr
+ if longrepr is None:
+ return
+
+ if hasattr(longrepr, "toterminal"):
+ longrepr_terminal = cast(TerminalRepr, longrepr)
+ longrepr_terminal.toterminal(out)
+ else:
+ try:
+ s = str(longrepr)
+ except UnicodeEncodeError:
+ s = ""
+ out.line(s)
+
+ def get_sections(self, prefix: str) -> Iterator[tuple[str, str]]:
+ for name, content in self.sections:
+ if name.startswith(prefix):
+ yield prefix, content
+
+ @property
+ def longreprtext(self) -> str:
+ """Read-only property that returns the full string representation of
+ ``longrepr``.
+
+ .. versionadded:: 3.0
+ """
+ file = StringIO()
+ tw = TerminalWriter(file)
+ tw.hasmarkup = False
+ self.toterminal(tw)
+ exc = file.getvalue()
+ return exc.strip()
+
+ @property
+ def caplog(self) -> str:
+ """Return captured log lines, if log capturing is enabled.
+
+ .. versionadded:: 3.5
+ """
+ return "\n".join(
+ content for (prefix, content) in self.get_sections("Captured log")
+ )
+
+ @property
+ def capstdout(self) -> str:
+ """Return captured text from stdout, if capturing is enabled.
+
+ .. versionadded:: 3.0
+ """
+ return "".join(
+ content for (prefix, content) in self.get_sections("Captured stdout")
+ )
+
+ @property
+ def capstderr(self) -> str:
+ """Return captured text from stderr, if capturing is enabled.
+
+ .. versionadded:: 3.0
+ """
+ return "".join(
+ content for (prefix, content) in self.get_sections("Captured stderr")
+ )
+
+ @property
+ def passed(self) -> bool:
+ """Whether the outcome is passed."""
+ return self.outcome == "passed"
+
+ @property
+ def failed(self) -> bool:
+ """Whether the outcome is failed."""
+ return self.outcome == "failed"
+
+ @property
+ def skipped(self) -> bool:
+ """Whether the outcome is skipped."""
+ return self.outcome == "skipped"
+
+ @property
+ def fspath(self) -> str:
+ """The path portion of the reported node, as a string."""
+ return self.nodeid.split("::")[0]
+
+ @property
+ def count_towards_summary(self) -> bool:
+ """**Experimental** Whether this report should be counted towards the
+ totals shown at the end of the test session: "1 passed, 1 failure, etc".
+
+ .. note::
+
+ This function is considered **experimental**, so beware that it is subject to changes
+ even in patch releases.
+ """
+ return True
+
+ @property
+ def head_line(self) -> str | None:
+ """**Experimental** The head line shown with longrepr output for this
+ report, more commonly during traceback representation during
+ failures::
+
+ ________ Test.foo ________
+
+
+ In the example above, the head_line is "Test.foo".
+
+ .. note::
+
+ This function is considered **experimental**, so beware that it is subject to changes
+ even in patch releases.
+ """
+ if self.location is not None:
+ _fspath, _lineno, domain = self.location
+ return domain
+ return None
+
+ def _get_verbose_word_with_markup(
+ self, config: Config, default_markup: Mapping[str, bool]
+ ) -> tuple[str, Mapping[str, bool]]:
+ _category, _short, verbose = config.hook.pytest_report_teststatus(
+ report=self, config=config
+ )
+
+ if isinstance(verbose, str):
+ return verbose, default_markup
+
+ if isinstance(verbose, Sequence) and len(verbose) == 2:
+ word, markup = verbose
+ if isinstance(word, str) and isinstance(markup, Mapping):
+ return word, markup
+
+ fail( # pragma: no cover
+ "pytest_report_teststatus() hook (from a plugin) returned "
+ f"an invalid verbose value: {verbose!r}.\nExpected either a string "
+ "or a tuple of (word, markup)."
+ )
+
+ def _to_json(self) -> dict[str, Any]:
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+
+ Experimental method.
+ """
+ return _report_to_json(self)
+
+ @classmethod
+ def _from_json(cls, reportdict: dict[str, object]) -> Self:
+ """Create either a TestReport or CollectReport, depending on the calling class.
+
+ It is the callers responsibility to know which class to pass here.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+
+ Experimental method.
+ """
+ kwargs = _report_kwargs_from_json(reportdict)
+ return cls(**kwargs)
+
+
+def _report_unserialization_failure(
+ type_name: str, report_class: type[BaseReport], reportdict
+) -> NoReturn:
+ url = "https://github.com/pytest-dev/pytest/issues"
+ stream = StringIO()
+ pprint("-" * 100, stream=stream)
+ pprint(f"INTERNALERROR: Unknown entry type returned: {type_name}", stream=stream)
+ pprint(f"report_name: {report_class}", stream=stream)
+ pprint(reportdict, stream=stream)
+ pprint(f"Please report this bug at {url}", stream=stream)
+ pprint("-" * 100, stream=stream)
+ raise RuntimeError(stream.getvalue())
+
+
+def _format_failed_longrepr(
+ item: Item, call: CallInfo[None], excinfo: ExceptionInfo[BaseException]
+):
+ if call.when == "call":
+ longrepr = item.repr_failure(excinfo)
+ else:
+ # Exception in setup or teardown.
+ longrepr = item._repr_failure_py(
+ excinfo, style=item.config.getoption("tbstyle", "auto")
+ )
+ return longrepr
+
+
+def _format_exception_group_all_skipped_longrepr(
+ item: Item,
+ excinfo: ExceptionInfo[BaseExceptionGroup[BaseException | BaseExceptionGroup]],
+) -> tuple[str, int, str]:
+ r = excinfo._getreprcrash()
+ assert r is not None, (
+ "There should always be a traceback entry for skipping a test."
+ )
+ if all(
+ getattr(skip, "_use_item_location", False) for skip in excinfo.value.exceptions
+ ):
+ path, line = item.reportinfo()[:2]
+ assert line is not None
+ loc = (os.fspath(path), line + 1)
+ default_msg = "skipped"
+ else:
+ loc = (str(r.path), r.lineno)
+ default_msg = r.message
+
+ # Get all unique skip messages.
+ msgs: list[str] = []
+ for exception in excinfo.value.exceptions:
+ m = getattr(exception, "msg", None) or (
+ exception.args[0] if exception.args else None
+ )
+ if m and m not in msgs:
+ msgs.append(m)
+
+ reason = "; ".join(msgs) if msgs else default_msg
+ longrepr = (*loc, reason)
+ return longrepr
+
+
+class TestReport(BaseReport):
+ """Basic test report object (also used for setup and teardown calls if
+ they fail).
+
+ Reports can contain arbitrary extra attributes.
+ """
+
+ __test__ = False
+
+ # Defined by skipping plugin.
+ # xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish.
+ wasxfail: str
+
+ def __init__(
+ self,
+ nodeid: str,
+ location: tuple[str, int | None, str],
+ keywords: Mapping[str, Any],
+ outcome: Literal["passed", "failed", "skipped"],
+ longrepr: None
+ | ExceptionInfo[BaseException]
+ | tuple[str, int, str]
+ | str
+ | TerminalRepr,
+ when: Literal["setup", "call", "teardown"],
+ sections: Iterable[tuple[str, str]] = (),
+ duration: float = 0,
+ start: float = 0,
+ stop: float = 0,
+ user_properties: Iterable[tuple[str, object]] | None = None,
+ **extra,
+ ) -> None:
+ #: Normalized collection nodeid.
+ self.nodeid = nodeid
+
+ #: A (filesystempath, lineno, domaininfo) tuple indicating the
+ #: actual location of a test item - it might be different from the
+ #: collected one e.g. if a method is inherited from a different module.
+ #: The filesystempath may be relative to ``config.rootdir``.
+ #: The line number is 0-based.
+ self.location: tuple[str, int | None, str] = location
+
+ #: A name -> value dictionary containing all keywords and
+ #: markers associated with a test invocation.
+ self.keywords: Mapping[str, Any] = keywords
+
+ #: Test outcome, always one of "passed", "failed", "skipped".
+ self.outcome = outcome
+
+ #: None or a failure representation.
+ self.longrepr = longrepr
+
+ #: One of 'setup', 'call', 'teardown' to indicate runtest phase.
+ self.when: Literal["setup", "call", "teardown"] = when
+
+ #: User properties is a list of tuples (name, value) that holds user
+ #: defined properties of the test.
+ self.user_properties = list(user_properties or [])
+
+ #: Tuples of str ``(heading, content)`` with extra information
+ #: for the test report. Used by pytest to add text captured
+ #: from ``stdout``, ``stderr``, and intercepted logging events. May
+ #: be used by other plugins to add arbitrary information to reports.
+ self.sections = list(sections)
+
+ #: Time it took to run just the test.
+ self.duration: float = duration
+
+ #: The system time when the call started, in seconds since the epoch.
+ self.start: float = start
+ #: The system time when the call ended, in seconds since the epoch.
+ self.stop: float = stop
+
+ self.__dict__.update(extra)
+
+ def __repr__(self) -> str:
+ return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>"
+
+ @classmethod
+ def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport:
+ """Create and fill a TestReport with standard item and call info.
+
+ :param item: The item.
+ :param call: The call info.
+ """
+ when = call.when
+ # Remove "collect" from the Literal type -- only for collection calls.
+ assert when != "collect"
+ duration = call.duration
+ start = call.start
+ stop = call.stop
+ keywords = {x: 1 for x in item.keywords}
+ excinfo = call.excinfo
+ sections = []
+ if not call.excinfo:
+ outcome: Literal["passed", "failed", "skipped"] = "passed"
+ longrepr: (
+ None
+ | ExceptionInfo[BaseException]
+ | tuple[str, int, str]
+ | str
+ | TerminalRepr
+ ) = None
+ else:
+ if not isinstance(excinfo, ExceptionInfo):
+ outcome = "failed"
+ longrepr = excinfo
+ elif isinstance(excinfo.value, skip.Exception):
+ outcome = "skipped"
+ r = excinfo._getreprcrash()
+ assert r is not None, (
+ "There should always be a traceback entry for skipping a test."
+ )
+ if excinfo.value._use_item_location:
+ path, line = item.reportinfo()[:2]
+ assert line is not None
+ longrepr = (os.fspath(path), line + 1, r.message)
+ else:
+ longrepr = (str(r.path), r.lineno, r.message)
+ elif isinstance(excinfo.value, BaseExceptionGroup) and (
+ excinfo.value.split(skip.Exception)[1] is None
+ ):
+ # All exceptions in the group are skip exceptions.
+ outcome = "skipped"
+ excinfo = cast(
+ ExceptionInfo[
+ BaseExceptionGroup[BaseException | BaseExceptionGroup]
+ ],
+ excinfo,
+ )
+ longrepr = _format_exception_group_all_skipped_longrepr(item, excinfo)
+ else:
+ outcome = "failed"
+ longrepr = _format_failed_longrepr(item, call, excinfo)
+ for rwhen, key, content in item._report_sections:
+ sections.append((f"Captured {key} {rwhen}", content))
+ return cls(
+ item.nodeid,
+ item.location,
+ keywords,
+ outcome,
+ longrepr,
+ when,
+ sections,
+ duration,
+ start,
+ stop,
+ user_properties=item.user_properties,
+ )
+
+
+@final
+class CollectReport(BaseReport):
+ """Collection report object.
+
+ Reports can contain arbitrary extra attributes.
+ """
+
+ when = "collect"
+
+ def __init__(
+ self,
+ nodeid: str,
+ outcome: Literal["passed", "failed", "skipped"],
+ longrepr: None
+ | ExceptionInfo[BaseException]
+ | tuple[str, int, str]
+ | str
+ | TerminalRepr,
+ result: list[Item | Collector] | None,
+ sections: Iterable[tuple[str, str]] = (),
+ **extra,
+ ) -> None:
+ #: Normalized collection nodeid.
+ self.nodeid = nodeid
+
+ #: Test outcome, always one of "passed", "failed", "skipped".
+ self.outcome = outcome
+
+ #: None or a failure representation.
+ self.longrepr = longrepr
+
+ #: The collected items and collection nodes.
+ self.result = result or []
+
+ #: Tuples of str ``(heading, content)`` with extra information
+ #: for the test report. Used by pytest to add text captured
+ #: from ``stdout``, ``stderr``, and intercepted logging events. May
+ #: be used by other plugins to add arbitrary information to reports.
+ self.sections = list(sections)
+
+ self.__dict__.update(extra)
+
+ @property
+ def location( # type:ignore[override]
+ self,
+ ) -> tuple[str, int | None, str] | None:
+ return (self.fspath, None, self.fspath)
+
+ def __repr__(self) -> str:
+ return f""
+
+
+class CollectErrorRepr(TerminalRepr):
+ def __init__(self, msg: str) -> None:
+ self.longrepr = msg
+
+ def toterminal(self, out: TerminalWriter) -> None:
+ out.line(self.longrepr, red=True)
+
+
+def pytest_report_to_serializable(
+ report: CollectReport | TestReport,
+) -> dict[str, Any] | None:
+ if isinstance(report, TestReport | CollectReport):
+ data = report._to_json()
+ data["$report_type"] = report.__class__.__name__
+ return data
+ # TODO: Check if this is actually reachable.
+ return None # type: ignore[unreachable]
+
+
+def pytest_report_from_serializable(
+ data: dict[str, Any],
+) -> CollectReport | TestReport | None:
+ if "$report_type" in data:
+ if data["$report_type"] == "TestReport":
+ return TestReport._from_json(data)
+ elif data["$report_type"] == "CollectReport":
+ return CollectReport._from_json(data)
+ assert False, "Unknown report_type unserialize data: {}".format(
+ data["$report_type"]
+ )
+ return None
+
+
+def _report_to_json(report: BaseReport) -> dict[str, Any]:
+ """Return the contents of this report as a dict of builtin entries,
+ suitable for serialization.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+ """
+
+ def serialize_repr_entry(
+ entry: ReprEntry | ReprEntryNative,
+ ) -> dict[str, Any]:
+ data = dataclasses.asdict(entry)
+ for key, value in data.items():
+ if hasattr(value, "__dict__"):
+ data[key] = dataclasses.asdict(value)
+ entry_data = {"type": type(entry).__name__, "data": data}
+ return entry_data
+
+ def serialize_repr_traceback(reprtraceback: ReprTraceback) -> dict[str, Any]:
+ result = dataclasses.asdict(reprtraceback)
+ result["reprentries"] = [
+ serialize_repr_entry(x) for x in reprtraceback.reprentries
+ ]
+ return result
+
+ def serialize_repr_crash(
+ reprcrash: ReprFileLocation | None,
+ ) -> dict[str, Any] | None:
+ if reprcrash is not None:
+ return dataclasses.asdict(reprcrash)
+ else:
+ return None
+
+ def serialize_exception_longrepr(rep: BaseReport) -> dict[str, Any]:
+ assert rep.longrepr is not None
+ # TODO: Investigate whether the duck typing is really necessary here.
+ longrepr = cast(ExceptionRepr, rep.longrepr)
+ result: dict[str, Any] = {
+ "reprcrash": serialize_repr_crash(longrepr.reprcrash),
+ "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback),
+ "sections": longrepr.sections,
+ }
+ if isinstance(longrepr, ExceptionChainRepr):
+ result["chain"] = []
+ for repr_traceback, repr_crash, description in longrepr.chain:
+ result["chain"].append(
+ (
+ serialize_repr_traceback(repr_traceback),
+ serialize_repr_crash(repr_crash),
+ description,
+ )
+ )
+ else:
+ result["chain"] = None
+ return result
+
+ d = report.__dict__.copy()
+ if hasattr(report.longrepr, "toterminal"):
+ if hasattr(report.longrepr, "reprtraceback") and hasattr(
+ report.longrepr, "reprcrash"
+ ):
+ d["longrepr"] = serialize_exception_longrepr(report)
+ else:
+ d["longrepr"] = str(report.longrepr)
+ else:
+ d["longrepr"] = report.longrepr
+ for name in d:
+ if isinstance(d[name], os.PathLike):
+ d[name] = os.fspath(d[name])
+ elif name == "result":
+ d[name] = None # for now
+ return d
+
+
+def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]:
+ """Return **kwargs that can be used to construct a TestReport or
+ CollectReport instance.
+
+ This was originally the serialize_report() function from xdist (ca03269).
+ """
+
+ def deserialize_repr_entry(entry_data):
+ data = entry_data["data"]
+ entry_type = entry_data["type"]
+ if entry_type == "ReprEntry":
+ reprfuncargs = None
+ reprfileloc = None
+ reprlocals = None
+ if data["reprfuncargs"]:
+ reprfuncargs = ReprFuncArgs(**data["reprfuncargs"])
+ if data["reprfileloc"]:
+ reprfileloc = ReprFileLocation(**data["reprfileloc"])
+ if data["reprlocals"]:
+ reprlocals = ReprLocals(data["reprlocals"]["lines"])
+
+ reprentry: ReprEntry | ReprEntryNative = ReprEntry(
+ lines=data["lines"],
+ reprfuncargs=reprfuncargs,
+ reprlocals=reprlocals,
+ reprfileloc=reprfileloc,
+ style=data["style"],
+ )
+ elif entry_type == "ReprEntryNative":
+ reprentry = ReprEntryNative(data["lines"])
+ else:
+ _report_unserialization_failure(entry_type, TestReport, reportdict)
+ return reprentry
+
+ def deserialize_repr_traceback(repr_traceback_dict):
+ repr_traceback_dict["reprentries"] = [
+ deserialize_repr_entry(x) for x in repr_traceback_dict["reprentries"]
+ ]
+ return ReprTraceback(**repr_traceback_dict)
+
+ def deserialize_repr_crash(repr_crash_dict: dict[str, Any] | None):
+ if repr_crash_dict is not None:
+ return ReprFileLocation(**repr_crash_dict)
+ else:
+ return None
+
+ if (
+ reportdict["longrepr"]
+ and "reprcrash" in reportdict["longrepr"]
+ and "reprtraceback" in reportdict["longrepr"]
+ ):
+ reprtraceback = deserialize_repr_traceback(
+ reportdict["longrepr"]["reprtraceback"]
+ )
+ reprcrash = deserialize_repr_crash(reportdict["longrepr"]["reprcrash"])
+ if reportdict["longrepr"]["chain"]:
+ chain = []
+ for repr_traceback_data, repr_crash_data, description in reportdict[
+ "longrepr"
+ ]["chain"]:
+ chain.append(
+ (
+ deserialize_repr_traceback(repr_traceback_data),
+ deserialize_repr_crash(repr_crash_data),
+ description,
+ )
+ )
+ exception_info: ExceptionChainRepr | ReprExceptionInfo = ExceptionChainRepr(
+ chain
+ )
+ else:
+ exception_info = ReprExceptionInfo(
+ reprtraceback=reprtraceback,
+ reprcrash=reprcrash,
+ )
+
+ for section in reportdict["longrepr"]["sections"]:
+ exception_info.addsection(*section)
+ reportdict["longrepr"] = exception_info
+
+ return reportdict
diff --git a/venv/lib/python3.11/site-packages/_pytest/runner.py b/venv/lib/python3.11/site-packages/_pytest/runner.py
new file mode 100644
index 000000000..3f03cfaff
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/runner.py
@@ -0,0 +1,593 @@
+# mypy: allow-untyped-defs
+"""Basic collect and runtest protocol implementations."""
+
+from __future__ import annotations
+
+import bdb
+from collections.abc import Callable
+import dataclasses
+import os
+import sys
+import types
+from typing import cast
+from typing import final
+from typing import Generic
+from typing import Literal
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+from .config import Config
+from .reports import BaseReport
+from .reports import CollectErrorRepr
+from .reports import CollectReport
+from .reports import TestReport
+from _pytest import timing
+from _pytest._code.code import ExceptionChainRepr
+from _pytest._code.code import ExceptionInfo
+from _pytest._code.code import TerminalRepr
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.nodes import Collector
+from _pytest.nodes import Directory
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.outcomes import Exit
+from _pytest.outcomes import OutcomeException
+from _pytest.outcomes import Skipped
+from _pytest.outcomes import TEST_OUTCOME
+
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+if TYPE_CHECKING:
+ from _pytest.main import Session
+ from _pytest.terminal import TerminalReporter
+
+#
+# pytest plugin hooks.
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting", "Reporting", after="general")
+ group.addoption(
+ "--durations",
+ action="store",
+ type=int,
+ default=None,
+ metavar="N",
+ help="Show N slowest setup/test durations (N=0 for all)",
+ )
+ group.addoption(
+ "--durations-min",
+ action="store",
+ type=float,
+ default=None,
+ metavar="N",
+ help="Minimal duration in seconds for inclusion in slowest list. "
+ "Default: 0.005 (or 0.0 if -vv is given).",
+ )
+
+
+def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
+ durations = terminalreporter.config.option.durations
+ durations_min = terminalreporter.config.option.durations_min
+ verbose = terminalreporter.config.get_verbosity()
+ if durations is None:
+ return
+ if durations_min is None:
+ durations_min = 0.005 if verbose < 2 else 0.0
+ tr = terminalreporter
+ dlist = []
+ for replist in tr.stats.values():
+ for rep in replist:
+ if hasattr(rep, "duration"):
+ dlist.append(rep)
+ if not dlist:
+ return
+ dlist.sort(key=lambda x: x.duration, reverse=True)
+ if not durations:
+ tr.write_sep("=", "slowest durations")
+ else:
+ tr.write_sep("=", f"slowest {durations} durations")
+ dlist = dlist[:durations]
+
+ for i, rep in enumerate(dlist):
+ if rep.duration < durations_min:
+ tr.write_line("")
+ message = f"({len(dlist) - i} durations < {durations_min:g}s hidden."
+ if terminalreporter.config.option.durations_min is None:
+ message += " Use -vv to show these durations."
+ message += ")"
+ tr.write_line(message)
+ break
+ tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}")
+
+
+def pytest_sessionstart(session: Session) -> None:
+ session._setupstate = SetupState()
+
+
+def pytest_sessionfinish(session: Session) -> None:
+ session._setupstate.teardown_exact(None)
+
+
+def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool:
+ ihook = item.ihook
+ ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
+ runtestprotocol(item, nextitem=nextitem)
+ ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
+ return True
+
+
+def runtestprotocol(
+ item: Item, log: bool = True, nextitem: Item | None = None
+) -> list[TestReport]:
+ hasrequest = hasattr(item, "_request")
+ if hasrequest and not item._request: # type: ignore[attr-defined]
+ # This only happens if the item is re-run, as is done by
+ # pytest-rerunfailures.
+ item._initrequest() # type: ignore[attr-defined]
+ try:
+ rep = call_and_report(item, "setup", log)
+ reports = [rep]
+ if rep.passed:
+ setup_only = item.config.getoption("setuponly", False)
+ if item.config.getoption("setupshow", False):
+ show_test_item(item, add_space=not setup_only)
+ if not setup_only:
+ reports.append(call_and_report(item, "call", log))
+ # If the session is about to fail or stop, teardown everything - this is
+ # necessary to correctly report fixture teardown errors (see #11706)
+ if item.session.shouldfail or item.session.shouldstop:
+ nextitem = None
+ reports.append(call_and_report(item, "teardown", log, nextitem=nextitem))
+ finally:
+ # After all teardown hooks have been called (or an exception was reraised)
+ # want funcargs and request info to go away.
+ if hasrequest:
+ item._request = False # type: ignore[attr-defined]
+ item.funcargs = None # type: ignore[attr-defined]
+ return reports
+
+
+def show_test_item(item: Item, *, add_space: bool) -> None:
+ """Show test function, parameters and the fixtures of the test item."""
+ tw = item.config.get_terminal_writer()
+ tw.line()
+ tw.write(" " * 8)
+ tw.write(item.nodeid)
+ used_fixtures = sorted(getattr(item, "fixturenames", []))
+ if used_fixtures:
+ tw.write(f" (fixtures used: {', '.join(used_fixtures)})")
+ if add_space:
+ tw.write(" ")
+ tw.flush()
+
+
+def pytest_runtest_setup(item: Item) -> None:
+ _update_current_test_var(item, "setup")
+ item.session._setupstate.setup(item)
+
+
+def pytest_runtest_call(item: Item) -> None:
+ _update_current_test_var(item, "call")
+ try:
+ del sys.last_type
+ del sys.last_value
+ del sys.last_traceback
+ if sys.version_info >= (3, 12, 0):
+ del sys.last_exc # type:ignore[attr-defined]
+ except AttributeError:
+ pass
+ try:
+ item.runtest()
+ except Exception as e:
+ # Store trace info to allow postmortem debugging
+ sys.last_type = type(e)
+ sys.last_value = e
+ if sys.version_info >= (3, 12, 0):
+ sys.last_exc = e # type:ignore[attr-defined]
+ assert e.__traceback__ is not None
+ # Skip *this* frame
+ sys.last_traceback = e.__traceback__.tb_next
+ raise
+
+
+def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None:
+ _update_current_test_var(item, "teardown")
+ item.session._setupstate.teardown_exact(nextitem)
+ _update_current_test_var(item, None)
+
+
+def _update_current_test_var(
+ item: Item, when: Literal["setup", "call", "teardown"] | None
+) -> None:
+ """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage.
+
+ If ``when`` is None, delete ``PYTEST_CURRENT_TEST`` from the environment.
+ """
+ var_name = "PYTEST_CURRENT_TEST"
+ if when:
+ value = f"{item.nodeid} ({when})"
+ # don't allow null bytes on environment variables (see #2644, #2957)
+ value = value.replace("\x00", "(null)")
+ os.environ[var_name] = value
+ else:
+ os.environ.pop(var_name)
+
+
+def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None:
+ if report.when in ("setup", "teardown"):
+ if report.failed:
+ # category, shortletter, verbose-word
+ return "error", "E", "ERROR"
+ elif report.skipped:
+ return "skipped", "s", "SKIPPED"
+ else:
+ return "", "", ""
+ return None
+
+
+#
+# Implementation
+
+
+def call_and_report(
+ item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds
+) -> TestReport:
+ ihook = item.ihook
+ if when == "setup":
+ runtest_hook: Callable[..., None] = ihook.pytest_runtest_setup
+ elif when == "call":
+ runtest_hook = ihook.pytest_runtest_call
+ elif when == "teardown":
+ runtest_hook = ihook.pytest_runtest_teardown
+ else:
+ assert False, f"Unhandled runtest hook case: {when}"
+
+ call = CallInfo.from_call(
+ lambda: runtest_hook(item=item, **kwds),
+ when=when,
+ reraise=get_reraise_exceptions(item.config),
+ )
+ report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call)
+ if log:
+ ihook.pytest_runtest_logreport(report=report)
+ if check_interactive_exception(call, report):
+ ihook.pytest_exception_interact(node=item, call=call, report=report)
+ return report
+
+
+def get_reraise_exceptions(config: Config) -> tuple[type[BaseException], ...]:
+ """Return exception types that should not be suppressed in general."""
+ reraise: tuple[type[BaseException], ...] = (Exit,)
+ if not config.getoption("usepdb", False):
+ reraise += (KeyboardInterrupt,)
+ return reraise
+
+
+def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> bool:
+ """Check whether the call raised an exception that should be reported as
+ interactive."""
+ if call.excinfo is None:
+ # Didn't raise.
+ return False
+ if hasattr(report, "wasxfail"):
+ # Exception was expected.
+ return False
+ unittest = sys.modules.get("unittest")
+ if isinstance(call.excinfo.value, Skipped | bdb.BdbQuit) or (
+ unittest is not None and isinstance(call.excinfo.value, unittest.SkipTest)
+ ):
+ # Special control flow exception.
+ return False
+ return True
+
+
+TResult = TypeVar("TResult", covariant=True)
+
+
+@final
+@dataclasses.dataclass
+class CallInfo(Generic[TResult]):
+ """Result/Exception info of a function invocation."""
+
+ _result: TResult | None
+ #: The captured exception of the call, if it raised.
+ excinfo: ExceptionInfo[BaseException] | None
+ #: The system time when the call started, in seconds since the epoch.
+ start: float
+ #: The system time when the call ended, in seconds since the epoch.
+ stop: float
+ #: The call duration, in seconds.
+ duration: float
+ #: The context of invocation: "collect", "setup", "call" or "teardown".
+ when: Literal["collect", "setup", "call", "teardown"]
+
+ def __init__(
+ self,
+ result: TResult | None,
+ excinfo: ExceptionInfo[BaseException] | None,
+ start: float,
+ stop: float,
+ duration: float,
+ when: Literal["collect", "setup", "call", "teardown"],
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._result = result
+ self.excinfo = excinfo
+ self.start = start
+ self.stop = stop
+ self.duration = duration
+ self.when = when
+
+ @property
+ def result(self) -> TResult:
+ """The return value of the call, if it didn't raise.
+
+ Can only be accessed if excinfo is None.
+ """
+ if self.excinfo is not None:
+ raise AttributeError(f"{self!r} has no valid result")
+ # The cast is safe because an exception wasn't raised, hence
+ # _result has the expected function return type (which may be
+ # None, that's why a cast and not an assert).
+ return cast(TResult, self._result)
+
+ @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
+ instant = timing.Instant()
+ try:
+ result: TResult | None = func()
+ except BaseException:
+ excinfo = ExceptionInfo.from_current()
+ if reraise is not None and isinstance(excinfo.value, reraise):
+ raise
+ result = None
+ duration = instant.elapsed()
+ return cls(
+ start=duration.start.time,
+ stop=duration.stop.time,
+ duration=duration.seconds,
+ when=when,
+ result=result,
+ excinfo=excinfo,
+ _ispytest=True,
+ )
+
+ def __repr__(self) -> str:
+ if self.excinfo is None:
+ return f""
+ return f""
+
+
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport:
+ return TestReport.from_item_and_call(item, call)
+
+
+def pytest_make_collect_report(collector: Collector) -> CollectReport:
+ def collect() -> list[Item | Collector]:
+ # Before collecting, if this is a Directory, load the conftests.
+ # If a conftest import fails to load, it is considered a collection
+ # error of the Directory collector. This is why it's done inside of the
+ # CallInfo wrapper.
+ #
+ # Note: initial conftests are loaded early, not here.
+ if isinstance(collector, Directory):
+ collector.config.pluginmanager._loadconftestmodules(
+ collector.path,
+ collector.config.getoption("importmode"),
+ rootpath=collector.config.rootpath,
+ consider_namespace_packages=collector.config.getini(
+ "consider_namespace_packages"
+ ),
+ )
+
+ return list(collector.collect())
+
+ call = CallInfo.from_call(
+ collect, "collect", reraise=(KeyboardInterrupt, SystemExit)
+ )
+ longrepr: None | tuple[str, int, str] | str | TerminalRepr = None
+ if not call.excinfo:
+ outcome: Literal["passed", "skipped", "failed"] = "passed"
+ else:
+ skip_exceptions = [Skipped]
+ unittest = sys.modules.get("unittest")
+ if unittest is not None:
+ skip_exceptions.append(unittest.SkipTest)
+ if isinstance(call.excinfo.value, tuple(skip_exceptions)):
+ outcome = "skipped"
+ r_ = collector._repr_failure_py(call.excinfo, "line")
+ assert isinstance(r_, ExceptionChainRepr), repr(r_)
+ r = r_.reprcrash
+ assert r
+ longrepr = (str(r.path), r.lineno, r.message)
+ else:
+ outcome = "failed"
+ errorinfo = collector.repr_failure(call.excinfo)
+ if not hasattr(errorinfo, "toterminal"):
+ assert isinstance(errorinfo, str)
+ errorinfo = CollectErrorRepr(errorinfo)
+ longrepr = errorinfo
+ result = call.result if not call.excinfo else None
+ rep = CollectReport(collector.nodeid, outcome, longrepr, result)
+ rep.call = call # type: ignore # see collect_one_node
+ return rep
+
+
+class SetupState:
+ """Shared state for setting up/tearing down test items or collectors
+ in a session.
+
+ Suppose we have a collection tree as follows:
+
+
+
+
+
+
+
+ The SetupState maintains a stack. The stack starts out empty:
+
+ []
+
+ During the setup phase of item1, setup(item1) is called. What it does
+ is:
+
+ push session to stack, run session.setup()
+ push mod1 to stack, run mod1.setup()
+ push item1 to stack, run item1.setup()
+
+ The stack is:
+
+ [session, mod1, item1]
+
+ While the stack is in this shape, it is allowed to add finalizers to
+ each of session, mod1, item1 using addfinalizer().
+
+ During the teardown phase of item1, teardown_exact(item2) is called,
+ where item2 is the next item to item1. What it does is:
+
+ pop item1 from stack, run its teardowns
+ pop mod1 from stack, run its teardowns
+
+ mod1 was popped because it ended its purpose with item1. The stack is:
+
+ [session]
+
+ During the setup phase of item2, setup(item2) is called. What it does
+ is:
+
+ push mod2 to stack, run mod2.setup()
+ push item2 to stack, run item2.setup()
+
+ Stack:
+
+ [session, mod2, item2]
+
+ During the teardown phase of item2, teardown_exact(None) is called,
+ because item2 is the last item. What it does is:
+
+ pop item2 from stack, run its teardowns
+ pop mod2 from stack, run its teardowns
+ pop session from stack, run its teardowns
+
+ Stack:
+
+ []
+
+ The end!
+ """
+
+ def __init__(self) -> None:
+ # The stack is in the dict insertion order.
+ self.stack: dict[
+ Node,
+ tuple[
+ # Node's finalizers.
+ list[Callable[[], object]],
+ # Node's exception and original traceback, if its setup raised.
+ tuple[OutcomeException | Exception, types.TracebackType | None] | None,
+ ],
+ ] = {}
+
+ def is_node_active(self, node: Node) -> bool:
+ """Check if a node is currently active in the stack -- set up and not
+ torn down yet."""
+ return node in self.stack
+
+ def setup(self, item: Item) -> None:
+ """Setup objects along the collector chain to the item."""
+ needed_collectors = item.listchain()
+
+ # If a collector fails its setup, fail its entire subtree of items.
+ # The setup is not retried for each item - the same exception is used.
+ for col, (finalizers, exc) in self.stack.items():
+ assert col in needed_collectors, "previous item was not torn down properly"
+ if exc:
+ raise exc[0].with_traceback(exc[1])
+
+ for col in needed_collectors[len(self.stack) :]:
+ assert col not in self.stack
+ # Push onto the stack.
+ self.stack[col] = ([col.teardown], None)
+ try:
+ col.setup()
+ except TEST_OUTCOME as exc:
+ self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__))
+ raise
+
+ def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None:
+ """Attach a finalizer to the given node.
+
+ The node must be currently active in the stack.
+ """
+ assert node and not isinstance(node, tuple)
+ assert callable(finalizer)
+ assert node in self.stack, (node, self.stack)
+ self.stack[node][0].append(finalizer)
+
+ def teardown_exact(self, nextitem: Item | None) -> None:
+ """Teardown the current stack up until reaching nodes that nextitem
+ also descends from.
+
+ When nextitem is None (meaning we're at the last item), the entire
+ stack is torn down.
+ """
+ needed_collectors = (nextitem and nextitem.listchain()) or []
+ exceptions: list[BaseException] = []
+ while self.stack:
+ if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
+ break
+ node, (finalizers, _) = self.stack.popitem()
+ these_exceptions = []
+ while finalizers:
+ fin = finalizers.pop()
+ try:
+ fin()
+ except TEST_OUTCOME as e:
+ these_exceptions.append(e)
+
+ if len(these_exceptions) == 1:
+ exceptions.extend(these_exceptions)
+ elif these_exceptions:
+ msg = f"errors while tearing down {node!r}"
+ exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1]))
+
+ if len(exceptions) == 1:
+ raise exceptions[0]
+ elif exceptions:
+ raise BaseExceptionGroup("errors during test teardown", exceptions[::-1])
+ if nextitem is None:
+ assert not self.stack
+
+
+def collect_one_node(collector: Collector) -> CollectReport:
+ ihook = collector.ihook
+ ihook.pytest_collectstart(collector=collector)
+ rep: CollectReport = ihook.pytest_make_collect_report(collector=collector)
+ call = rep.__dict__.pop("call", None)
+ if call and check_interactive_exception(call, rep):
+ ihook.pytest_exception_interact(node=collector, call=call, report=rep)
+ return rep
diff --git a/venv/lib/python3.11/site-packages/_pytest/scope.py b/venv/lib/python3.11/site-packages/_pytest/scope.py
new file mode 100644
index 000000000..68a9da0e6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/scope.py
@@ -0,0 +1,91 @@
+"""
+Scope definition and related utilities.
+
+Those are defined here, instead of in the 'fixtures' module because
+their use is spread across many other pytest modules, and centralizing it in 'fixtures'
+would cause circular references.
+
+Also this makes the module light to import, as it should.
+"""
+
+from __future__ import annotations
+
+from enum import Enum
+from functools import total_ordering
+from typing import Literal
+
+
+ScopeName = Literal["session", "package", "module", "class", "function"]
+
+
+@total_ordering
+class Scope(Enum):
+ """
+ Represents one of the possible fixture scopes in pytest.
+
+ Scopes are ordered from lower to higher, that is:
+
+ ->>> higher ->>>
+
+ Function < Class < Module < Package < Session
+
+ <<<- lower <<<-
+ """
+
+ # Scopes need to be listed from lower to higher.
+ Function = "function"
+ Class = "class"
+ Module = "module"
+ Package = "package"
+ Session = "session"
+
+ def next_lower(self) -> Scope:
+ """Return the next lower scope."""
+ index = _SCOPE_INDICES[self]
+ if index == 0:
+ raise ValueError(f"{self} is the lower-most scope")
+ return _ALL_SCOPES[index - 1]
+
+ def next_higher(self) -> Scope:
+ """Return the next higher scope."""
+ index = _SCOPE_INDICES[self]
+ if index == len(_SCOPE_INDICES) - 1:
+ raise ValueError(f"{self} is the upper-most scope")
+ return _ALL_SCOPES[index + 1]
+
+ def __lt__(self, other: Scope) -> bool:
+ self_index = _SCOPE_INDICES[self]
+ other_index = _SCOPE_INDICES[other]
+ return self_index < other_index
+
+ @classmethod
+ def from_user(
+ cls, scope_name: ScopeName, descr: str, where: str | None = None
+ ) -> Scope:
+ """
+ Given a scope name from the user, return the equivalent Scope enum. Should be used
+ whenever we want to convert a user provided scope name to its enum object.
+
+ If the scope name is invalid, construct a user friendly message and call pytest.fail.
+ """
+ from _pytest.outcomes import fail
+
+ try:
+ # Holding this reference is necessary for mypy at the moment.
+ scope = Scope(scope_name)
+ except ValueError:
+ fail(
+ "{} {}got an unexpected scope value '{}'".format(
+ descr, f"from {where} " if where else "", scope_name
+ ),
+ pytrace=False,
+ )
+ return scope
+
+
+_ALL_SCOPES = list(Scope)
+_SCOPE_INDICES = {scope: index for index, scope in enumerate(_ALL_SCOPES)}
+
+
+# Ordered list of scopes which can contain many tests (in practice all except Function).
+HIGH_SCOPES = [x for x in Scope if x is not Scope.Function]
diff --git a/venv/lib/python3.11/site-packages/_pytest/setuponly.py b/venv/lib/python3.11/site-packages/_pytest/setuponly.py
new file mode 100644
index 000000000..7e6b46bcd
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/setuponly.py
@@ -0,0 +1,98 @@
+from __future__ import annotations
+
+from collections.abc import Generator
+
+from _pytest._io.saferepr import saferepr
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureDef
+from _pytest.fixtures import SubRequest
+from _pytest.scope import Scope
+import pytest
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--setuponly",
+ "--setup-only",
+ action="store_true",
+ help="Only setup fixtures, do not execute tests",
+ )
+ group.addoption(
+ "--setupshow",
+ "--setup-show",
+ action="store_true",
+ help="Show setup of fixtures while executing tests",
+ )
+
+
+@pytest.hookimpl(wrapper=True)
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[object], request: SubRequest
+) -> Generator[None, object, object]:
+ try:
+ return (yield)
+ finally:
+ if request.config.option.setupshow:
+ if hasattr(request, "param"):
+ # Save the fixture parameter so ._show_fixture_action() can
+ # display it now and during the teardown (in .finish()).
+ if fixturedef.ids:
+ if callable(fixturedef.ids):
+ param = fixturedef.ids(request.param)
+ else:
+ param = fixturedef.ids[request.param_index]
+ else:
+ param = request.param
+ fixturedef.cached_param = param # type: ignore[attr-defined]
+ _show_fixture_action(fixturedef, request.config, "SETUP")
+
+
+def pytest_fixture_post_finalizer(
+ fixturedef: FixtureDef[object], request: SubRequest
+) -> None:
+ if fixturedef.cached_result is not None:
+ config = request.config
+ if config.option.setupshow:
+ _show_fixture_action(fixturedef, request.config, "TEARDOWN")
+ if hasattr(fixturedef, "cached_param"):
+ del fixturedef.cached_param
+
+
+def _show_fixture_action(
+ fixturedef: FixtureDef[object], config: Config, msg: str
+) -> None:
+ capman = config.pluginmanager.getplugin("capturemanager")
+ if capman:
+ capman.suspend_global_capture()
+
+ tw = config.get_terminal_writer()
+ tw.line()
+ # Use smaller indentation the higher the scope: Session = 0, Package = 1, etc.
+ scope_indent = list(reversed(Scope)).index(fixturedef._scope)
+ tw.write(" " * 2 * scope_indent)
+
+ scopename = fixturedef.scope[0].upper()
+ tw.write(f"{msg:<8} {scopename} {fixturedef.argname}")
+
+ if msg == "SETUP":
+ deps = sorted(arg for arg in fixturedef.argnames if arg != "request")
+ if deps:
+ tw.write(" (fixtures used: {})".format(", ".join(deps)))
+
+ if hasattr(fixturedef, "cached_param"):
+ tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]")
+
+ tw.flush()
+
+ if capman:
+ capman.resume_global_capture()
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
+ if config.option.setuponly:
+ config.option.setupshow = True
+ return None
diff --git a/venv/lib/python3.11/site-packages/_pytest/setupplan.py b/venv/lib/python3.11/site-packages/_pytest/setupplan.py
new file mode 100644
index 000000000..4e124cce2
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/setupplan.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config.argparsing import Parser
+from _pytest.fixtures import FixtureDef
+from _pytest.fixtures import SubRequest
+import pytest
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("debugconfig")
+ group.addoption(
+ "--setupplan",
+ "--setup-plan",
+ action="store_true",
+ help="Show what fixtures and tests would be executed but "
+ "don't execute anything",
+ )
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_fixture_setup(
+ fixturedef: FixtureDef[object], request: SubRequest
+) -> object | None:
+ # Will return a dummy fixture if the setuponly option is provided.
+ if request.config.option.setupplan:
+ my_cache_key = fixturedef.cache_key(request)
+ fixturedef.cached_result = (None, my_cache_key, None)
+ return fixturedef.cached_result
+ return None
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_cmdline_main(config: Config) -> int | ExitCode | None:
+ if config.option.setupplan:
+ config.option.setuponly = True
+ config.option.setupshow = True
+ return None
diff --git a/venv/lib/python3.11/site-packages/_pytest/skipping.py b/venv/lib/python3.11/site-packages/_pytest/skipping.py
new file mode 100644
index 000000000..f7a4c4c04
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/skipping.py
@@ -0,0 +1,321 @@
+# mypy: allow-untyped-defs
+"""Support for skip/xfail functions and markers."""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+from collections.abc import Mapping
+import dataclasses
+import os
+import platform
+import sys
+import traceback
+
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.mark.structures import Mark
+from _pytest.nodes import Item
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.outcomes import xfail
+from _pytest.raises import AbstractRaises
+from _pytest.reports import BaseReport
+from _pytest.reports import TestReport
+from _pytest.runner import CallInfo
+from _pytest.stash import StashKey
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--runxfail",
+ action="store_true",
+ dest="runxfail",
+ default=False,
+ help="Report the results of xfail tests as if they were not marked",
+ )
+
+ parser.addini(
+ "strict_xfail",
+ "Default for the strict parameter of xfail "
+ "markers when not given explicitly (default: False) (alias: xfail_strict)",
+ type="bool",
+ # None => fallback to `strict`.
+ default=None,
+ aliases=["xfail_strict"],
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ if config.option.runxfail:
+ # yay a hack
+ import pytest
+
+ old = pytest.xfail
+ config.add_cleanup(lambda: setattr(pytest, "xfail", old))
+
+ def nop(*args, **kwargs):
+ pass
+
+ nop.Exception = xfail.Exception # type: ignore[attr-defined]
+ setattr(pytest, "xfail", nop)
+
+ config.addinivalue_line(
+ "markers",
+ "skip(reason=None): skip the given test function with an optional reason. "
+ 'Example: skip(reason="no way of currently testing this") skips the '
+ "test.",
+ )
+ config.addinivalue_line(
+ "markers",
+ "skipif(condition, ..., *, reason=...): "
+ "skip the given test function if any of the conditions evaluate to True. "
+ "Example: skipif(sys.platform == 'win32') skips the test if we are on the win32 platform. "
+ "See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-skipif",
+ )
+ config.addinivalue_line(
+ "markers",
+ "xfail(condition, ..., *, reason=..., run=True, raises=None, strict=strict_xfail): "
+ "mark the test function as an expected failure if any of the conditions "
+ "evaluate to True. Optionally specify a reason for better reporting "
+ "and run=False if you don't even want to execute the test function. "
+ "If only specific exception(s) are expected, you can list them in "
+ "raises, and if the test fails in other ways, it will be reported as "
+ "a true failure. See https://docs.pytest.org/en/stable/reference/reference.html#pytest-mark-xfail",
+ )
+
+
+def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool, str]:
+ """Evaluate a single skipif/xfail condition.
+
+ If an old-style string condition is given, it is eval()'d, otherwise the
+ condition is bool()'d. If this fails, an appropriately formatted pytest.fail
+ is raised.
+
+ Returns (result, reason). The reason is only relevant if the result is True.
+ """
+ # String condition.
+ if isinstance(condition, str):
+ globals_ = {
+ "os": os,
+ "sys": sys,
+ "platform": platform,
+ "config": item.config,
+ }
+ for dictionary in reversed(
+ item.ihook.pytest_markeval_namespace(config=item.config)
+ ):
+ if not isinstance(dictionary, Mapping):
+ raise ValueError(
+ f"pytest_markeval_namespace() needs to return a dict, got {dictionary!r}"
+ )
+ globals_.update(dictionary)
+ if hasattr(item, "obj"):
+ globals_.update(item.obj.__globals__)
+ try:
+ filename = f"<{mark.name} condition>"
+ condition_code = compile(condition, filename, "eval")
+ result = eval(condition_code, globals_)
+ except SyntaxError as exc:
+ msglines = [
+ f"Error evaluating {mark.name!r} condition",
+ " " + condition,
+ " " + " " * (exc.offset or 0) + "^",
+ "SyntaxError: invalid syntax",
+ ]
+ fail("\n".join(msglines), pytrace=False)
+ except Exception as exc:
+ msglines = [
+ f"Error evaluating {mark.name!r} condition",
+ " " + condition,
+ *traceback.format_exception_only(exc),
+ ]
+ fail("\n".join(msglines), pytrace=False)
+
+ # Boolean condition.
+ else:
+ try:
+ result = bool(condition)
+ except Exception as exc:
+ msglines = [
+ f"Error evaluating {mark.name!r} condition as a boolean",
+ *traceback.format_exception_only(exc),
+ ]
+ fail("\n".join(msglines), pytrace=False)
+
+ reason = mark.kwargs.get("reason", None)
+ if reason is None:
+ if isinstance(condition, str):
+ reason = "condition: " + condition
+ else:
+ # XXX better be checked at collection time
+ msg = (
+ f"Error evaluating {mark.name!r}: "
+ + "you need to specify reason=STRING when using booleans as conditions."
+ )
+ fail(msg, pytrace=False)
+
+ return result, reason
+
+
+@dataclasses.dataclass(frozen=True)
+class Skip:
+ """The result of evaluate_skip_marks()."""
+
+ reason: str = "unconditional skip"
+
+
+def evaluate_skip_marks(item: Item) -> Skip | None:
+ """Evaluate skip and skipif marks on item, returning Skip if triggered."""
+ for mark in item.iter_markers(name="skipif"):
+ if "condition" not in mark.kwargs:
+ conditions = mark.args
+ else:
+ conditions = (mark.kwargs["condition"],)
+
+ # Unconditional.
+ if not conditions:
+ reason = mark.kwargs.get("reason", "")
+ return Skip(reason)
+
+ # If any of the conditions are true.
+ for condition in conditions:
+ result, reason = evaluate_condition(item, mark, condition)
+ if result:
+ return Skip(reason)
+
+ for mark in item.iter_markers(name="skip"):
+ try:
+ return Skip(*mark.args, **mark.kwargs)
+ except TypeError as e:
+ raise TypeError(str(e) + " - maybe you meant pytest.mark.skipif?") from None
+
+ return None
+
+
+@dataclasses.dataclass(frozen=True)
+class Xfail:
+ """The result of evaluate_xfail_marks()."""
+
+ __slots__ = ("raises", "reason", "run", "strict")
+
+ reason: str
+ run: bool
+ strict: bool
+ raises: (
+ type[BaseException]
+ | tuple[type[BaseException], ...]
+ | AbstractRaises[BaseException]
+ | None
+ )
+
+
+def evaluate_xfail_marks(item: Item) -> Xfail | None:
+ """Evaluate xfail marks on item, returning Xfail if triggered."""
+ for mark in item.iter_markers(name="xfail"):
+ run = mark.kwargs.get("run", True)
+ strict = mark.kwargs.get("strict")
+ if strict is None:
+ strict = item.config.getini("strict_xfail")
+ if strict is None:
+ strict = item.config.getini("strict")
+ raises = mark.kwargs.get("raises", None)
+ if "condition" not in mark.kwargs:
+ conditions = mark.args
+ else:
+ conditions = (mark.kwargs["condition"],)
+
+ # Unconditional.
+ if not conditions:
+ reason = mark.kwargs.get("reason", "")
+ return Xfail(reason, run, strict, raises)
+
+ # If any of the conditions are true.
+ for condition in conditions:
+ result, reason = evaluate_condition(item, mark, condition)
+ if result:
+ return Xfail(reason, run, strict, raises)
+
+ return None
+
+
+# Saves the xfail mark evaluation. Can be refreshed during call if None.
+xfailed_key = StashKey[Xfail | None]()
+
+
+@hookimpl(tryfirst=True)
+def pytest_runtest_setup(item: Item) -> None:
+ skipped = evaluate_skip_marks(item)
+ if skipped:
+ raise skip.Exception(skipped.reason, _use_item_location=True)
+
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+ if xfailed and not item.config.option.runxfail and not xfailed.run:
+ xfail("[NOTRUN] " + xfailed.reason)
+
+
+@hookimpl(wrapper=True)
+def pytest_runtest_call(item: Item) -> Generator[None]:
+ xfailed = item.stash.get(xfailed_key, None)
+ if xfailed is None:
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+
+ if xfailed and not item.config.option.runxfail and not xfailed.run:
+ xfail("[NOTRUN] " + xfailed.reason)
+
+ try:
+ return (yield)
+ finally:
+ # The test run may have added an xfail mark dynamically.
+ xfailed = item.stash.get(xfailed_key, None)
+ if xfailed is None:
+ item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item)
+
+
+@hookimpl(wrapper=True)
+def pytest_runtest_makereport(
+ item: Item, call: CallInfo[None]
+) -> Generator[None, TestReport, TestReport]:
+ rep = yield
+ xfailed = item.stash.get(xfailed_key, None)
+ if item.config.option.runxfail:
+ pass # don't interfere
+ elif call.excinfo and isinstance(call.excinfo.value, xfail.Exception):
+ assert call.excinfo.value.msg is not None
+ rep.wasxfail = call.excinfo.value.msg
+ rep.outcome = "skipped"
+ elif not rep.skipped and xfailed:
+ if call.excinfo:
+ raises = xfailed.raises
+ if raises is None or (
+ (
+ isinstance(raises, type | tuple)
+ and isinstance(call.excinfo.value, raises)
+ )
+ or (
+ isinstance(raises, AbstractRaises)
+ and raises.matches(call.excinfo.value)
+ )
+ ):
+ rep.outcome = "skipped"
+ rep.wasxfail = xfailed.reason
+ else:
+ rep.outcome = "failed"
+ elif call.when == "call":
+ if xfailed.strict:
+ rep.outcome = "failed"
+ rep.longrepr = "[XPASS(strict)] " + xfailed.reason
+ else:
+ rep.outcome = "passed"
+ rep.wasxfail = xfailed.reason
+ return rep
+
+
+def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None:
+ if hasattr(report, "wasxfail"):
+ if report.skipped:
+ return "xfailed", "x", "XFAIL"
+ elif report.passed:
+ return "xpassed", "X", "XPASS"
+ return None
diff --git a/venv/lib/python3.11/site-packages/_pytest/stash.py b/venv/lib/python3.11/site-packages/_pytest/stash.py
new file mode 100644
index 000000000..6a9ff884e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/stash.py
@@ -0,0 +1,116 @@
+from __future__ import annotations
+
+from typing import Any
+from typing import cast
+from typing import Generic
+from typing import TypeVar
+
+
+__all__ = ["Stash", "StashKey"]
+
+
+T = TypeVar("T")
+D = TypeVar("D")
+
+
+class StashKey(Generic[T]):
+ """``StashKey`` is an object used as a key to a :class:`Stash`.
+
+ A ``StashKey`` is associated with the type ``T`` of the value of the key.
+
+ A ``StashKey`` is unique and cannot conflict with another key.
+
+ .. versionadded:: 7.0
+ """
+
+ __slots__ = ()
+
+
+class Stash:
+ r"""``Stash`` is a type-safe heterogeneous mutable mapping that
+ allows keys and value types to be defined separately from
+ where it (the ``Stash``) is created.
+
+ Usually you will be given an object which has a ``Stash``, for example
+ :class:`~pytest.Config` or a :class:`~_pytest.nodes.Node`:
+
+ .. code-block:: python
+
+ stash: Stash = some_object.stash
+
+ If a module or plugin wants to store data in this ``Stash``, it creates
+ :class:`StashKey`\s for its keys (at the module level):
+
+ .. code-block:: python
+
+ # At the top-level of the module
+ some_str_key = StashKey[str]()
+ some_bool_key = StashKey[bool]()
+
+ To store information:
+
+ .. code-block:: python
+
+ # Value type must match the key.
+ stash[some_str_key] = "value"
+ stash[some_bool_key] = True
+
+ To retrieve the information:
+
+ .. code-block:: python
+
+ # The static type of some_str is str.
+ some_str = stash[some_str_key]
+ # The static type of some_bool is bool.
+ some_bool = stash[some_bool_key]
+
+ .. versionadded:: 7.0
+ """
+
+ __slots__ = ("_storage",)
+
+ def __init__(self) -> None:
+ self._storage: dict[StashKey[Any], object] = {}
+
+ def __setitem__(self, key: StashKey[T], value: T) -> None:
+ """Set a value for key."""
+ self._storage[key] = value
+
+ def __getitem__(self, key: StashKey[T]) -> T:
+ """Get the value for key.
+
+ Raises ``KeyError`` if the key wasn't set before.
+ """
+ return cast(T, self._storage[key])
+
+ def get(self, key: StashKey[T], default: D) -> T | D:
+ """Get the value for key, or return default if the key wasn't set
+ before."""
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
+ def setdefault(self, key: StashKey[T], default: T) -> T:
+ """Return the value of key if already set, otherwise set the value
+ of key to default and return default."""
+ try:
+ return self[key]
+ except KeyError:
+ self[key] = default
+ return default
+
+ def __delitem__(self, key: StashKey[T]) -> None:
+ """Delete the value for key.
+
+ Raises ``KeyError`` if the key wasn't set before.
+ """
+ del self._storage[key]
+
+ def __contains__(self, key: StashKey[T]) -> bool:
+ """Return whether key was set."""
+ return key in self._storage
+
+ def __len__(self) -> int:
+ """Return how many items exist in the stash."""
+ return len(self._storage)
diff --git a/venv/lib/python3.11/site-packages/_pytest/stepwise.py b/venv/lib/python3.11/site-packages/_pytest/stepwise.py
new file mode 100644
index 000000000..8901540eb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/stepwise.py
@@ -0,0 +1,209 @@
+from __future__ import annotations
+
+import dataclasses
+from datetime import datetime
+from datetime import timedelta
+from typing import Any
+from typing import TYPE_CHECKING
+
+from _pytest import nodes
+from _pytest.cacheprovider import Cache
+from _pytest.config import Config
+from _pytest.config.argparsing import Parser
+from _pytest.main import Session
+from _pytest.reports import TestReport
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+STEPWISE_CACHE_DIR = "cache/stepwise"
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("general")
+ group.addoption(
+ "--sw",
+ "--stepwise",
+ action="store_true",
+ default=False,
+ dest="stepwise",
+ help="Exit on test failure and continue from last failing test next time",
+ )
+ group.addoption(
+ "--sw-skip",
+ "--stepwise-skip",
+ action="store_true",
+ default=False,
+ dest="stepwise_skip",
+ help="Ignore the first failing test but stop on the next failing test. "
+ "Implicitly enables --stepwise.",
+ )
+ group.addoption(
+ "--sw-reset",
+ "--stepwise-reset",
+ action="store_true",
+ default=False,
+ dest="stepwise_reset",
+ help="Resets stepwise state, restarting the stepwise workflow. "
+ "Implicitly enables --stepwise.",
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ # --stepwise-skip/--stepwise-reset implies stepwise.
+ if config.option.stepwise_skip or config.option.stepwise_reset:
+ config.option.stepwise = True
+ if config.getoption("stepwise"):
+ config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")
+
+
+def pytest_sessionfinish(session: Session) -> None:
+ if not session.config.getoption("stepwise"):
+ assert session.config.cache is not None
+ if hasattr(session.config, "workerinput"):
+ # Do not update cache if this process is a xdist worker to prevent
+ # race conditions (#10641).
+ return
+
+
+@dataclasses.dataclass
+class StepwiseCacheInfo:
+ # The nodeid of the last failed test.
+ last_failed: str | None
+
+ # The number of tests in the last time --stepwise was run.
+ # We use this information as a simple way to invalidate the cache information, avoiding
+ # confusing behavior in case the cache is stale.
+ last_test_count: int | None
+
+ # The date when the cache was last updated, for information purposes only.
+ last_cache_date_str: str
+
+ @property
+ def last_cache_date(self) -> datetime:
+ return datetime.fromisoformat(self.last_cache_date_str)
+
+ @classmethod
+ def empty(cls) -> Self:
+ return cls(
+ last_failed=None,
+ last_test_count=None,
+ last_cache_date_str=datetime.now().isoformat(),
+ )
+
+ def update_date_to_now(self) -> None:
+ self.last_cache_date_str = datetime.now().isoformat()
+
+
+class StepwisePlugin:
+ def __init__(self, config: Config) -> None:
+ self.config = config
+ self.session: Session | None = None
+ self.report_status: list[str] = []
+ assert config.cache is not None
+ self.cache: Cache = config.cache
+ self.skip: bool = config.getoption("stepwise_skip")
+ self.reset: bool = config.getoption("stepwise_reset")
+ self.cached_info = self._load_cached_info()
+
+ def _load_cached_info(self) -> StepwiseCacheInfo:
+ cached_dict: dict[str, Any] | None = self.cache.get(STEPWISE_CACHE_DIR, None)
+ if cached_dict:
+ try:
+ return StepwiseCacheInfo(
+ cached_dict["last_failed"],
+ cached_dict["last_test_count"],
+ cached_dict["last_cache_date_str"],
+ )
+ except (KeyError, TypeError) as e:
+ error = f"{type(e).__name__}: {e}"
+ self.report_status.append(f"error reading cache, discarding ({error})")
+
+ # Cache not found or error during load, return a new cache.
+ return StepwiseCacheInfo.empty()
+
+ def pytest_sessionstart(self, session: Session) -> None:
+ self.session = session
+
+ def pytest_collection_modifyitems(
+ self, config: Config, items: list[nodes.Item]
+ ) -> None:
+ last_test_count = self.cached_info.last_test_count
+ self.cached_info.last_test_count = len(items)
+
+ if self.reset:
+ self.report_status.append("resetting state, not skipping.")
+ self.cached_info.last_failed = None
+ return
+
+ if not self.cached_info.last_failed:
+ self.report_status.append("no previously failed tests, not skipping.")
+ return
+
+ if last_test_count is not None and last_test_count != len(items):
+ self.report_status.append(
+ f"test count changed, not skipping (now {len(items)} tests, previously {last_test_count})."
+ )
+ self.cached_info.last_failed = None
+ return
+
+ # Check all item nodes until we find a match on last failed.
+ failed_index = None
+ for index, item in enumerate(items):
+ if item.nodeid == self.cached_info.last_failed:
+ failed_index = index
+ break
+
+ # If the previously failed test was not found among the test items,
+ # do not skip any tests.
+ if failed_index is None:
+ self.report_status.append("previously failed test not found, not skipping.")
+ else:
+ cache_age = datetime.now() - self.cached_info.last_cache_date
+ # Round up to avoid showing microseconds.
+ cache_age = timedelta(seconds=int(cache_age.total_seconds()))
+ self.report_status.append(
+ f"skipping {failed_index} already passed items (cache from {cache_age} ago,"
+ f" use --sw-reset to discard)."
+ )
+ deselected = items[:failed_index]
+ del items[:failed_index]
+ config.hook.pytest_deselected(items=deselected)
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ if report.failed:
+ if self.skip:
+ # Remove test from the failed ones (if it exists) and unset the skip option
+ # to make sure the following tests will not be skipped.
+ if report.nodeid == self.cached_info.last_failed:
+ self.cached_info.last_failed = None
+
+ self.skip = False
+ else:
+ # Mark test as the last failing and interrupt the test session.
+ self.cached_info.last_failed = report.nodeid
+ assert self.session is not None
+ self.session.shouldstop = (
+ "Test failed, continuing from this test next run."
+ )
+
+ else:
+ # If the test was actually run and did pass.
+ if report.when == "call":
+ # Remove test from the failed ones, if exists.
+ if report.nodeid == self.cached_info.last_failed:
+ self.cached_info.last_failed = None
+
+ def pytest_report_collectionfinish(self) -> list[str] | None:
+ if self.config.get_verbosity() >= 0 and self.report_status:
+ return [f"stepwise: {x}" for x in self.report_status]
+ return None
+
+ def pytest_sessionfinish(self) -> None:
+ if hasattr(self.config, "workerinput"):
+ # Do not update cache if this process is a xdist worker to prevent
+ # race conditions (#10641).
+ return
+ self.cached_info.update_date_to_now()
+ self.cache.set(STEPWISE_CACHE_DIR, dataclasses.asdict(self.cached_info))
diff --git a/venv/lib/python3.11/site-packages/_pytest/subtests.py b/venv/lib/python3.11/site-packages/_pytest/subtests.py
new file mode 100644
index 000000000..6ac3b5cd0
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/subtests.py
@@ -0,0 +1,418 @@
+"""Builtin plugin that adds subtests support."""
+
+from __future__ import annotations
+
+from collections import defaultdict
+from collections.abc import Callable
+from collections.abc import Iterator
+from collections.abc import Mapping
+from contextlib import AbstractContextManager
+from contextlib import contextmanager
+from contextlib import ExitStack
+from contextlib import nullcontext
+import dataclasses
+import time
+from types import TracebackType
+from typing import Any
+from typing import TYPE_CHECKING
+
+import pluggy
+
+from _pytest._code import ExceptionInfo
+from _pytest._io.saferepr import saferepr
+from _pytest.capture import CaptureFixture
+from _pytest.capture import FDCapture
+from _pytest.capture import SysCapture
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import SubRequest
+from _pytest.logging import catching_logs
+from _pytest.logging import LogCaptureHandler
+from _pytest.logging import LoggingPlugin
+from _pytest.reports import TestReport
+from _pytest.runner import CallInfo
+from _pytest.runner import check_interactive_exception
+from _pytest.runner import get_reraise_exceptions
+from _pytest.stash import StashKey
+
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
+
+
+def pytest_addoption(parser: Parser) -> None:
+ Config._add_verbosity_ini(
+ parser,
+ Config.VERBOSITY_SUBTESTS,
+ help=(
+ "Specify verbosity level for subtests. "
+ "Higher levels will generate output for passed subtests. Failed subtests are always reported."
+ ),
+ )
+
+
+@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
+class SubtestContext:
+ """The values passed to Subtests.test() that are included in the test report."""
+
+ msg: str | None
+ kwargs: Mapping[str, Any]
+
+ def __post_init__(self) -> None:
+ # Brute-force the returned kwargs dict to be JSON serializable (pytest-dev/pytest-xdist#1273).
+ object.__setattr__(
+ self, "kwargs", {k: saferepr(v) for (k, v) in self.kwargs.items()}
+ )
+
+ def _to_json(self) -> dict[str, Any]:
+ result = dataclasses.asdict(self)
+ return result
+
+ @classmethod
+ def _from_json(cls, d: dict[str, Any]) -> Self:
+ return cls(msg=d["msg"], kwargs=d["kwargs"])
+
+
+@dataclasses.dataclass(init=False)
+class SubtestReport(TestReport):
+ context: SubtestContext
+
+ @property
+ def head_line(self) -> str:
+ _, _, domain = self.location
+ return f"{domain} {self._sub_test_description()}"
+
+ def _sub_test_description(self) -> str:
+ parts = []
+ if self.context.msg is not None:
+ parts.append(f"[{self.context.msg}]")
+ if self.context.kwargs:
+ params_desc = ", ".join(
+ f"{k}={v}" for (k, v) in self.context.kwargs.items()
+ )
+ parts.append(f"({params_desc})")
+ return " ".join(parts) or "()"
+
+ def _to_json(self) -> dict[str, Any]:
+ data = super()._to_json()
+ del data["context"]
+ data["_report_type"] = "SubTestReport"
+ data["_subtest.context"] = self.context._to_json()
+ return data
+
+ @classmethod
+ def _from_json(cls, reportdict: dict[str, Any]) -> SubtestReport:
+ report = super()._from_json(reportdict)
+ report.context = SubtestContext._from_json(reportdict["_subtest.context"])
+ return report
+
+ @classmethod
+ def _new(
+ cls,
+ test_report: TestReport,
+ context: SubtestContext,
+ captured_output: Captured | None,
+ captured_logs: CapturedLogs | None,
+ ) -> Self:
+ result = super()._from_json(test_report._to_json())
+ result.context = context
+
+ if captured_output:
+ if captured_output.out:
+ result.sections.append(("Captured stdout call", captured_output.out))
+ if captured_output.err:
+ result.sections.append(("Captured stderr call", captured_output.err))
+
+ if captured_logs and (log := captured_logs.handler.stream.getvalue()):
+ result.sections.append(("Captured log call", log))
+
+ return result
+
+
+@fixture
+def subtests(request: SubRequest) -> Subtests:
+ """Provides subtests functionality."""
+ capmam = request.node.config.pluginmanager.get_plugin("capturemanager")
+ suspend_capture_ctx = (
+ capmam.global_and_fixture_disabled if capmam is not None else nullcontext
+ )
+ return Subtests(request.node.ihook, suspend_capture_ctx, request, _ispytest=True)
+
+
+class Subtests:
+ """Subtests fixture, enables declaring subtests inside test functions via the :meth:`test` method."""
+
+ def __init__(
+ self,
+ ihook: pluggy.HookRelay,
+ suspend_capture_ctx: Callable[[], AbstractContextManager[None]],
+ request: SubRequest,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ self._ihook = ihook
+ self._suspend_capture_ctx = suspend_capture_ctx
+ self._request = request
+
+ def test(
+ self,
+ msg: str | None = None,
+ **kwargs: Any,
+ ) -> _SubTestContextManager:
+ """
+ Context manager for subtests, capturing exceptions raised inside the subtest scope and
+ reporting assertion failures and errors individually.
+
+ Usage
+ -----
+
+ .. code-block:: python
+
+ def test(subtests):
+ for i in range(5):
+ with subtests.test("custom message", i=i):
+ assert i % 2 == 0
+
+ :param msg:
+ If given, the message will be shown in the test report in case of subtest failure.
+
+ :param kwargs:
+ Arbitrary values that are also added to the subtest report.
+ """
+ return _SubTestContextManager(
+ self._ihook,
+ msg,
+ kwargs,
+ request=self._request,
+ suspend_capture_ctx=self._suspend_capture_ctx,
+ config=self._request.config,
+ )
+
+
+@dataclasses.dataclass
+class _SubTestContextManager:
+ """
+ Context manager for subtests, capturing exceptions raised inside the subtest scope and handling
+ them through the pytest machinery.
+ """
+
+ # Note: initially the logic for this context manager was implemented directly
+ # in Subtests.test() as a @contextmanager, however, it is not possible to control the output fully when
+ # exiting from it due to an exception when in `--exitfirst` mode, so this was refactored into an
+ # explicit context manager class (pytest-dev/pytest-subtests#134).
+
+ ihook: pluggy.HookRelay
+ msg: str | None
+ kwargs: dict[str, Any]
+ suspend_capture_ctx: Callable[[], AbstractContextManager[None]]
+ request: SubRequest
+ config: Config
+
+ def __enter__(self) -> None:
+ __tracebackhide__ = True
+
+ self._start = time.time()
+ self._precise_start = time.perf_counter()
+ self._exc_info = None
+
+ self._exit_stack = ExitStack()
+ self._captured_output = self._exit_stack.enter_context(
+ capturing_output(self.request)
+ )
+ self._captured_logs = self._exit_stack.enter_context(
+ capturing_logs(self.request)
+ )
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ __tracebackhide__ = True
+ if exc_val is not None:
+ exc_info = ExceptionInfo.from_exception(exc_val)
+ else:
+ exc_info = None
+
+ self._exit_stack.close()
+
+ precise_stop = time.perf_counter()
+ duration = precise_stop - self._precise_start
+ stop = time.time()
+
+ call_info = CallInfo[None](
+ None,
+ exc_info,
+ start=self._start,
+ stop=stop,
+ duration=duration,
+ when="call",
+ _ispytest=True,
+ )
+ report = self.ihook.pytest_runtest_makereport(
+ item=self.request.node, call=call_info
+ )
+ sub_report = SubtestReport._new(
+ report,
+ SubtestContext(msg=self.msg, kwargs=self.kwargs),
+ captured_output=self._captured_output,
+ captured_logs=self._captured_logs,
+ )
+
+ if sub_report.failed:
+ failed_subtests = self.config.stash[failed_subtests_key]
+ failed_subtests[self.request.node.nodeid] += 1
+
+ with self.suspend_capture_ctx():
+ self.ihook.pytest_runtest_logreport(report=sub_report)
+
+ if check_interactive_exception(call_info, sub_report):
+ self.ihook.pytest_exception_interact(
+ node=self.request.node, call=call_info, report=sub_report
+ )
+
+ if exc_val is not None:
+ if isinstance(exc_val, get_reraise_exceptions(self.config)):
+ return False
+ if self.request.session.shouldfail:
+ return False
+ return True
+
+
+@contextmanager
+def capturing_output(request: SubRequest) -> Iterator[Captured]:
+ option = request.config.getoption("capture", None)
+
+ capman = request.config.pluginmanager.getplugin("capturemanager")
+ if getattr(capman, "_capture_fixture", None):
+ # capsys or capfd are active, subtest should not capture.
+ fixture = None
+ elif option == "sys":
+ fixture = CaptureFixture(SysCapture, request, _ispytest=True)
+ elif option == "fd":
+ fixture = CaptureFixture(FDCapture, request, _ispytest=True)
+ else:
+ fixture = None
+
+ if fixture is not None:
+ fixture._start()
+
+ captured = Captured()
+ try:
+ yield captured
+ finally:
+ if fixture is not None:
+ out, err = fixture.readouterr()
+ fixture.close()
+ captured.out = out
+ captured.err = err
+
+
+@contextmanager
+def capturing_logs(
+ request: SubRequest,
+) -> Iterator[CapturedLogs | None]:
+ logging_plugin: LoggingPlugin | None = request.config.pluginmanager.getplugin(
+ "logging-plugin"
+ )
+ if logging_plugin is None:
+ yield None
+ else:
+ handler = LogCaptureHandler()
+ handler.setFormatter(logging_plugin.formatter)
+
+ captured_logs = CapturedLogs(handler)
+ with catching_logs(handler, level=logging_plugin.log_level):
+ yield captured_logs
+
+
+@dataclasses.dataclass
+class Captured:
+ out: str = ""
+ err: str = ""
+
+
+@dataclasses.dataclass
+class CapturedLogs:
+ handler: LogCaptureHandler
+
+
+def pytest_report_to_serializable(report: TestReport) -> dict[str, Any] | None:
+ if isinstance(report, SubtestReport):
+ return report._to_json()
+ return None
+
+
+def pytest_report_from_serializable(data: dict[str, Any]) -> SubtestReport | None:
+ if data.get("_report_type") == "SubTestReport":
+ return SubtestReport._from_json(data)
+ return None
+
+
+# Dict of nodeid -> number of failed subtests.
+# Used to fail top-level tests that passed but contain failed subtests.
+failed_subtests_key = StashKey[defaultdict[str, int]]()
+
+
+def pytest_configure(config: Config) -> None:
+ config.stash[failed_subtests_key] = defaultdict(int)
+
+
+@hookimpl(tryfirst=True)
+def pytest_report_teststatus(
+ report: TestReport,
+ config: Config,
+) -> tuple[str, str, str | Mapping[str, bool]] | None:
+ if report.when != "call":
+ return None
+
+ quiet = config.get_verbosity(Config.VERBOSITY_SUBTESTS) == 0
+ if isinstance(report, SubtestReport):
+ outcome = report.outcome
+ description = report._sub_test_description()
+
+ if hasattr(report, "wasxfail"):
+ if quiet:
+ return "", "", ""
+ elif outcome == "skipped":
+ category = "xfailed"
+ short = "y" # x letter is used for regular xfail, y for subtest xfail
+ status = "SUBXFAIL"
+ # outcome == "passed" in an xfail is only possible via a @pytest.mark.xfail mark, which
+ # is not applicable to a subtest, which only handles pytest.xfail().
+ else: # pragma: no cover
+ # This should not normally happen, unless some plugin is setting wasxfail without
+ # the correct outcome. Pytest expects the call outcome to be either skipped or
+ # passed in case of xfail.
+ # Let's pass this report to the next hook.
+ return None
+ return category, short, f"{status}{description}"
+
+ if report.failed:
+ return outcome, "u", f"SUBFAILED{description}"
+ else:
+ if report.passed:
+ if quiet:
+ return "", "", ""
+ else:
+ return f"subtests {outcome}", "u", f"SUBPASSED{description}"
+ elif report.skipped:
+ if quiet:
+ return "", "", ""
+ else:
+ return outcome, "-", f"SUBSKIPPED{description}"
+
+ else:
+ failed_subtests_count = config.stash[failed_subtests_key][report.nodeid]
+ # Top-level test, fail if it contains failed subtests and it has passed.
+ if report.passed and failed_subtests_count > 0:
+ report.outcome = "failed"
+ suffix = "s" if failed_subtests_count > 1 else ""
+ report.longrepr = f"contains {failed_subtests_count} failed subtest{suffix}"
+
+ return None
diff --git a/venv/lib/python3.11/site-packages/_pytest/terminal.py b/venv/lib/python3.11/site-packages/_pytest/terminal.py
new file mode 100644
index 000000000..b9a65ff19
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/terminal.py
@@ -0,0 +1,1786 @@
+# mypy: allow-untyped-defs
+"""Terminal reporting of the full testing process.
+
+This is a good source for looking at the various reporting hooks.
+"""
+
+from __future__ import annotations
+
+import argparse
+from collections import Counter
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Mapping
+from collections.abc import Sequence
+import dataclasses
+import datetime
+from functools import partial
+import inspect
+from pathlib import Path
+import platform
+import sys
+import textwrap
+from typing import Any
+from typing import ClassVar
+from typing import final
+from typing import Literal
+from typing import NamedTuple
+from typing import TextIO
+from typing import TYPE_CHECKING
+import warnings
+
+import pluggy
+
+from _pytest import compat
+from _pytest import nodes
+from _pytest import timing
+from _pytest._code import ExceptionInfo
+from _pytest._code.code import ExceptionRepr
+from _pytest._io import TerminalWriter
+from _pytest._io.wcwidth import wcswidth
+import _pytest._version
+from _pytest.compat import running_on_ci
+from _pytest.config import _PluggyPlugin
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.nodes import Item
+from _pytest.nodes import Node
+from _pytest.pathlib import absolutepath
+from _pytest.pathlib import bestrelpath
+from _pytest.reports import BaseReport
+from _pytest.reports import CollectReport
+from _pytest.reports import TestReport
+
+
+if TYPE_CHECKING:
+ from _pytest.main import Session
+
+
+REPORT_COLLECTING_RESOLUTION = 0.5
+
+KNOWN_TYPES = (
+ "failed",
+ "passed",
+ "skipped",
+ "deselected",
+ "xfailed",
+ "xpassed",
+ "warnings",
+ "error",
+ "subtests passed",
+ "subtests failed",
+ "subtests skipped",
+)
+
+_REPORTCHARS_DEFAULT = "fE"
+
+
+class MoreQuietAction(argparse.Action):
+ """A modified copy of the argparse count action which counts down and updates
+ the legacy quiet attribute at the same time.
+
+ Used to unify verbosity handling.
+ """
+
+ def __init__(
+ self,
+ option_strings: Sequence[str],
+ dest: str,
+ default: object = None,
+ required: bool = False,
+ help: str | None = None,
+ ) -> None:
+ super().__init__(
+ option_strings=option_strings,
+ dest=dest,
+ nargs=0,
+ default=default,
+ required=required,
+ help=help,
+ )
+
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: str | Sequence[object] | None,
+ option_string: str | None = None,
+ ) -> None:
+ new_count = getattr(namespace, self.dest, 0) - 1
+ setattr(namespace, self.dest, new_count)
+ # todo Deprecate config.quiet
+ namespace.quiet = getattr(namespace, "quiet", 0) + 1
+
+
+class TestShortLogReport(NamedTuple):
+ """Used to store the test status result category, shortletter and verbose word.
+ For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
+
+ :ivar category:
+ The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string.
+
+ :ivar letter:
+ The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
+
+ :ivar word:
+ Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
+ ``"ERROR"``, or the empty string.
+ """
+
+ category: str
+ letter: str
+ word: str | tuple[str, Mapping[str, bool]]
+
+
+def pytest_addoption(parser: Parser) -> None:
+ group = parser.getgroup("terminal reporting", "Reporting", after="general")
+ group._addoption( # private to use reserved lower-case short option
+ "-v",
+ "--verbose",
+ action="count",
+ default=0,
+ dest="verbose",
+ help="Increase verbosity",
+ )
+ group.addoption(
+ "--no-header",
+ action="store_true",
+ default=False,
+ dest="no_header",
+ help="Disable header",
+ )
+ group.addoption(
+ "--no-summary",
+ action="store_true",
+ default=False,
+ dest="no_summary",
+ help="Disable summary",
+ )
+ group.addoption(
+ "--no-fold-skipped",
+ action="store_false",
+ dest="fold_skipped",
+ default=True,
+ help="Do not fold skipped tests in short summary.",
+ )
+ group.addoption(
+ "--force-short-summary",
+ action="store_true",
+ dest="force_short_summary",
+ default=False,
+ help="Force condensed summary output regardless of verbosity level.",
+ )
+ group._addoption( # private to use reserved lower-case short option
+ "-q",
+ "--quiet",
+ action=MoreQuietAction,
+ default=0,
+ dest="verbose",
+ help="Decrease verbosity",
+ )
+ group.addoption(
+ "--verbosity",
+ dest="verbose",
+ type=int,
+ default=0,
+ help="Set verbosity. Default: 0.",
+ )
+ group._addoption( # private to use reserved lower-case short option
+ "-r",
+ "--report-chars",
+ action="store",
+ dest="reportchars",
+ default=_REPORTCHARS_DEFAULT,
+ metavar="chars",
+ help="Show extra test summary info as specified by chars: (f)ailed, "
+ "(E)rror, (s)kipped, (x)failed, (X)passed, "
+ "(p)assed, (P)assed with output, (a)ll except passed (p/P), or (A)ll. "
+ "(w)arnings are enabled by default (see --disable-warnings), "
+ "'N' can be used to reset the list. (default: 'fE').",
+ )
+ group.addoption(
+ "--disable-warnings",
+ "--disable-pytest-warnings",
+ default=False,
+ dest="disable_warnings",
+ action="store_true",
+ help="Disable warnings summary",
+ )
+ group._addoption( # private to use reserved lower-case short option
+ "-l",
+ "--showlocals",
+ action="store_true",
+ dest="showlocals",
+ default=False,
+ help="Show locals in tracebacks (disabled by default)",
+ )
+ group.addoption(
+ "--no-showlocals",
+ action="store_false",
+ dest="showlocals",
+ help="Hide locals in tracebacks (negate --showlocals passed through addopts)",
+ )
+ group.addoption(
+ "--tb",
+ metavar="style",
+ action="store",
+ dest="tbstyle",
+ default="auto",
+ choices=["auto", "long", "short", "no", "line", "native"],
+ help="Traceback print mode (auto/long/short/line/native/no)",
+ )
+ group.addoption(
+ "--xfail-tb",
+ action="store_true",
+ dest="xfail_tb",
+ default=False,
+ help="Show tracebacks for xfail (as long as --tb != no)",
+ )
+ group.addoption(
+ "--show-capture",
+ action="store",
+ dest="showcapture",
+ choices=["no", "stdout", "stderr", "log", "all"],
+ default="all",
+ help="Controls how captured stdout/stderr/log is shown on failed tests. "
+ "Default: all.",
+ )
+ group.addoption(
+ "--fulltrace",
+ "--full-trace",
+ action="store_true",
+ default=False,
+ help="Don't cut any tracebacks (default is to cut)",
+ )
+ group.addoption(
+ "--color",
+ metavar="color",
+ action="store",
+ dest="color",
+ default="auto",
+ choices=["yes", "no", "auto"],
+ help="Color terminal output (yes/no/auto)",
+ )
+ group.addoption(
+ "--code-highlight",
+ default="yes",
+ choices=["yes", "no"],
+ help="Whether code should be highlighted (only if --color is also enabled). "
+ "Default: yes.",
+ )
+
+ parser.addini(
+ "console_output_style",
+ help='Console output: "classic", or with additional progress information '
+ '("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces '
+ "progress even when capture=no)",
+ default="progress",
+ )
+ Config._add_verbosity_ini(
+ parser,
+ Config.VERBOSITY_TEST_CASES,
+ help=(
+ "Specify a verbosity level for test case execution, overriding the main level. "
+ "Higher levels will provide more detailed information about each test case executed."
+ ),
+ )
+
+
+def pytest_configure(config: Config) -> None:
+ reporter = TerminalReporter(config, sys.stdout)
+ config.pluginmanager.register(reporter, "terminalreporter")
+ if config.option.debug or config.option.traceconfig:
+
+ def mywriter(tags, args):
+ msg = " ".join(map(str, args))
+ reporter.write_line("[traceconfig] " + msg)
+
+ config.trace.root.setprocessor("pytest:config", mywriter)
+
+ # See terminalprogress.py.
+ # On Windows it's safe to load by default.
+ if sys.platform == "win32":
+ config.pluginmanager.import_plugin("terminalprogress")
+
+
+def getreportopt(config: Config) -> str:
+ reportchars: str = config.option.reportchars
+
+ old_aliases = {"F", "S"}
+ reportopts = ""
+ for char in reportchars:
+ if char in old_aliases:
+ char = char.lower()
+ if char == "a":
+ reportopts = "sxXEf"
+ elif char == "A":
+ reportopts = "PpsxXEf"
+ elif char == "N":
+ reportopts = ""
+ elif char not in reportopts:
+ reportopts += char
+
+ if not config.option.disable_warnings and "w" not in reportopts:
+ reportopts = "w" + reportopts
+ elif config.option.disable_warnings and "w" in reportopts:
+ reportopts = reportopts.replace("w", "")
+
+ return reportopts
+
+
+@hookimpl(trylast=True) # after _pytest.runner
+def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str]:
+ letter = "F"
+ if report.passed:
+ letter = "."
+ elif report.skipped:
+ letter = "s"
+
+ outcome: str = report.outcome
+ if report.when in ("collect", "setup", "teardown") and outcome == "failed":
+ outcome = "error"
+ letter = "E"
+
+ return outcome, letter, outcome.upper()
+
+
+@dataclasses.dataclass
+class WarningReport:
+ """Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
+
+ :ivar str message:
+ User friendly message about the warning.
+ :ivar str|None nodeid:
+ nodeid that generated the warning (see ``get_location``).
+ :ivar tuple fslocation:
+ File system location of the source of the warning (see ``get_location``).
+ """
+
+ message: str
+ nodeid: str | None = None
+ fslocation: tuple[str, int] | None = None
+
+ count_towards_summary: ClassVar = True
+
+ def get_location(self, config: Config) -> str | None:
+ """Return the more user-friendly information about the location of a warning, or None."""
+ if self.nodeid:
+ return self.nodeid
+ if self.fslocation:
+ filename, linenum = self.fslocation
+ relpath = bestrelpath(config.invocation_params.dir, absolutepath(filename))
+ return f"{relpath}:{linenum}"
+ return None
+
+
+@final
+class TerminalReporter:
+ def __init__(self, config: Config, file: TextIO | None = None) -> None:
+ import _pytest.config
+
+ self.config = config
+ self._numcollected = 0
+ self._session: Session | None = None
+ self._showfspath: bool | None = None
+
+ self.stats: dict[str, list[Any]] = {}
+ self._main_color: str | None = None
+ self._known_types: list[str] | None = None
+ self.startpath = config.invocation_params.dir
+ if file is None:
+ file = sys.stdout
+ self._tw = _pytest.config.create_terminal_writer(config, file)
+ self._screen_width = self._tw.fullwidth
+ self.currentfspath: None | Path | str | int = None
+ self.reportchars = getreportopt(config)
+ self.foldskipped = config.option.fold_skipped
+ self.hasmarkup = self._tw.hasmarkup
+ # isatty should be a method but was wrongly implemented as a boolean.
+ # We use CallableBool here to support both.
+ self.isatty = compat.CallableBool(file.isatty())
+ self._progress_nodeids_reported: set[str] = set()
+ self._timing_nodeids_reported: set[str] = set()
+ self._show_progress_info = self._determine_show_progress_info()
+ self._collect_report_last_write = timing.Instant()
+ self._already_displayed_warnings: int | None = None
+ self._keyboardinterrupt_memo: ExceptionRepr | None = None
+
+ def _determine_show_progress_info(
+ self,
+ ) -> Literal["progress", "count", "times", False]:
+ """Return whether we should display progress information based on the current config."""
+ # do not show progress if we are not capturing output (#3038) unless explicitly
+ # overridden by progress-even-when-capture-no
+ if (
+ self.config.getoption("capture", "no") == "no"
+ and self.config.getini("console_output_style")
+ != "progress-even-when-capture-no"
+ ):
+ return False
+ # do not show progress if we are showing fixture setup/teardown
+ if self.config.getoption("setupshow", False):
+ return False
+ cfg: str = self.config.getini("console_output_style")
+ if cfg in {"progress", "progress-even-when-capture-no"}:
+ return "progress"
+ elif cfg == "count":
+ return "count"
+ elif cfg == "times":
+ return "times"
+ else:
+ return False
+
+ @property
+ def verbosity(self) -> int:
+ verbosity: int = self.config.option.verbose
+ return verbosity
+
+ @property
+ def showheader(self) -> bool:
+ return self.verbosity >= 0
+
+ @property
+ def no_header(self) -> bool:
+ return bool(self.config.option.no_header)
+
+ @property
+ def no_summary(self) -> bool:
+ return bool(self.config.option.no_summary)
+
+ @property
+ def showfspath(self) -> bool:
+ if self._showfspath is None:
+ return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) >= 0
+ return self._showfspath
+
+ @showfspath.setter
+ def showfspath(self, value: bool | None) -> None:
+ self._showfspath = value
+
+ @property
+ def showlongtestinfo(self) -> bool:
+ return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) > 0
+
+ @property
+ def reported_progress(self) -> int:
+ """The amount of items reported in the progress so far.
+
+ :meta private:
+ """
+ return len(self._progress_nodeids_reported)
+
+ def hasopt(self, char: str) -> bool:
+ char = {"xfailed": "x", "skipped": "s"}.get(char, char)
+ return char in self.reportchars
+
+ def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None:
+ fspath = self.config.rootpath / nodeid.split("::", maxsplit=1)[0]
+ if self.currentfspath is None or fspath != self.currentfspath:
+ if self.currentfspath is not None and self._show_progress_info:
+ self._write_progress_information_filling_space()
+ self.currentfspath = fspath
+ relfspath = bestrelpath(self.startpath, fspath)
+ self._tw.line()
+ self._tw.write(relfspath + " ")
+ self._tw.write(res, flush=True, **markup)
+
+ def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None:
+ if self.currentfspath != prefix:
+ self._tw.line()
+ self.currentfspath = prefix
+ self._tw.write(prefix)
+ if extra:
+ self._tw.write(extra, **kwargs)
+ self.currentfspath = -2
+
+ def ensure_newline(self) -> None:
+ if self.currentfspath:
+ self._tw.line()
+ self.currentfspath = None
+
+ def wrap_write(
+ self,
+ content: str,
+ *,
+ flush: bool = False,
+ margin: int = 8,
+ line_sep: str = "\n",
+ **markup: bool,
+ ) -> None:
+ """Wrap message with margin for progress info."""
+ width_of_current_line = self._tw.width_of_current_line
+ wrapped = line_sep.join(
+ textwrap.wrap(
+ " " * width_of_current_line + content,
+ width=self._screen_width - margin,
+ drop_whitespace=True,
+ replace_whitespace=False,
+ ),
+ )
+ wrapped = wrapped[width_of_current_line:]
+ self._tw.write(wrapped, flush=flush, **markup)
+
+ def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
+ self._tw.write(content, flush=flush, **markup)
+
+ def write_raw(self, content: str, *, flush: bool = False) -> None:
+ self._tw.write_raw(content, flush=flush)
+
+ def flush(self) -> None:
+ self._tw.flush()
+
+ def write_line(self, line: str | bytes, **markup: bool) -> None:
+ if not isinstance(line, str):
+ line = str(line, errors="replace")
+ self.ensure_newline()
+ self._tw.line(line, **markup)
+
+ def rewrite(self, line: str, **markup: bool) -> None:
+ """Rewinds the terminal cursor to the beginning and writes the given line.
+
+ :param erase:
+ If True, will also add spaces until the full terminal width to ensure
+ previous lines are properly erased.
+
+ The rest of the keyword arguments are markup instructions.
+ """
+ erase = markup.pop("erase", False)
+ if erase:
+ fill_count = self._tw.fullwidth - len(line) - 1
+ fill = " " * fill_count
+ else:
+ fill = ""
+ line = str(line)
+ self._tw.write("\r" + line + fill, **markup)
+
+ def write_sep(
+ self,
+ sep: str,
+ title: str | None = None,
+ fullwidth: int | None = None,
+ **markup: bool,
+ ) -> None:
+ self.ensure_newline()
+ self._tw.sep(sep, title, fullwidth, **markup)
+
+ def section(self, title: str, sep: str = "=", **kw: bool) -> None:
+ self._tw.sep(sep, title, **kw)
+
+ def line(self, msg: str, **kw: bool) -> None:
+ self._tw.line(msg, **kw)
+
+ def _add_stats(self, category: str, items: Sequence[Any]) -> None:
+ set_main_color = category not in self.stats
+ self.stats.setdefault(category, []).extend(items)
+ if set_main_color:
+ self._set_main_color()
+
+ def pytest_internalerror(self, excrepr: ExceptionRepr) -> bool:
+ for line in str(excrepr).split("\n"):
+ self.write_line("INTERNALERROR> " + line)
+ return True
+
+ def pytest_warning_recorded(
+ self,
+ warning_message: warnings.WarningMessage,
+ nodeid: str,
+ ) -> None:
+ from _pytest.warnings import warning_record_to_str
+
+ fslocation = warning_message.filename, warning_message.lineno
+ message = warning_record_to_str(warning_message)
+
+ warning_report = WarningReport(
+ fslocation=fslocation, message=message, nodeid=nodeid
+ )
+ self._add_stats("warnings", [warning_report])
+
+ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
+ if self.config.option.traceconfig:
+ msg = f"PLUGIN registered: {plugin}"
+ # XXX This event may happen during setup/teardown time
+ # which unfortunately captures our output here
+ # which garbles our output if we use self.write_line.
+ self.write_line(msg)
+
+ def pytest_deselected(self, items: Sequence[Item]) -> None:
+ self._add_stats("deselected", items)
+
+ def pytest_runtest_logstart(
+ self, nodeid: str, location: tuple[str, int | None, str]
+ ) -> None:
+ fspath, lineno, domain = location
+ # Ensure that the path is printed before the
+ # 1st test of a module starts running.
+ if self.showlongtestinfo:
+ line = self._locationline(nodeid, fspath, lineno, domain)
+ self.write_ensure_prefix(line, "")
+ self.flush()
+ elif self.showfspath:
+ self.write_fspath_result(nodeid, "")
+ self.flush()
+
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ self._tests_ran = True
+ rep = report
+
+ res = TestShortLogReport(
+ *self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
+ )
+ category, letter, word = res.category, res.letter, res.word
+ if not isinstance(word, tuple):
+ markup = None
+ else:
+ word, markup = word
+ self._add_stats(category, [rep])
+ if not letter and not word:
+ # Probably passed setup/teardown.
+ return
+ if markup is None:
+ was_xfail = hasattr(report, "wasxfail")
+ if rep.passed and not was_xfail:
+ markup = {"green": True}
+ elif rep.passed and was_xfail:
+ markup = {"yellow": True}
+ elif rep.failed:
+ markup = {"red": True}
+ elif rep.skipped:
+ markup = {"yellow": True}
+ else:
+ markup = {}
+ self._progress_nodeids_reported.add(rep.nodeid)
+ if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0:
+ self._tw.write(letter, **markup)
+ # When running in xdist, the logreport and logfinish of multiple
+ # items are interspersed, e.g. `logreport`, `logreport`,
+ # `logfinish`, `logfinish`. To avoid the "past edge" calculation
+ # from getting confused and overflowing (#7166), do the past edge
+ # printing here and not in logfinish, except for the 100% which
+ # should only be printed after all teardowns are finished.
+ if self._show_progress_info and not self._is_last_item:
+ self._write_progress_information_if_past_edge()
+ else:
+ line = self._locationline(rep.nodeid, *rep.location)
+ running_xdist = hasattr(rep, "node")
+ if not running_xdist:
+ self.write_ensure_prefix(line, word, **markup)
+ if rep.skipped or hasattr(report, "wasxfail"):
+ reason = _get_raw_skip_reason(rep)
+ if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) < 2:
+ available_width = (
+ (self._tw.fullwidth - self._tw.width_of_current_line)
+ - len(" [100%]")
+ - 1
+ )
+ formatted_reason = _format_trimmed(
+ " ({})", reason, available_width
+ )
+ else:
+ formatted_reason = f" ({reason})"
+
+ if reason and formatted_reason is not None:
+ self.wrap_write(formatted_reason)
+ if self._show_progress_info:
+ self._write_progress_information_filling_space()
+ else:
+ self.ensure_newline()
+ self._tw.write(f"[{rep.node.gateway.id}]")
+ if self._show_progress_info:
+ self._tw.write(
+ self._get_progress_information_message() + " ", cyan=True
+ )
+ else:
+ self._tw.write(" ")
+ self._tw.write(word, **markup)
+ self._tw.write(" " + line)
+ self.currentfspath = -2
+ self.flush()
+
+ @property
+ def _is_last_item(self) -> bool:
+ assert self._session is not None
+ return self.reported_progress == self._session.testscollected
+
+ @hookimpl(wrapper=True)
+ def pytest_runtestloop(self) -> Generator[None, object, object]:
+ result = yield
+
+ # Write the final/100% progress -- deferred until the loop is complete.
+ if (
+ self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0
+ and self._show_progress_info
+ and self.reported_progress
+ ):
+ self._write_progress_information_filling_space()
+
+ return result
+
+ def _get_progress_information_message(self) -> str:
+ assert self._session
+ collected = self._session.testscollected
+ if self._show_progress_info == "count":
+ if collected:
+ progress = self.reported_progress
+ counter_format = f"{{:{len(str(collected))}d}}"
+ format_string = f" [{counter_format}/{{}}]"
+ return format_string.format(progress, collected)
+ return f" [ {collected} / {collected} ]"
+ if self._show_progress_info == "times":
+ if not collected:
+ return ""
+ all_reports = (
+ self._get_reports_to_display("passed")
+ + self._get_reports_to_display("xpassed")
+ + self._get_reports_to_display("failed")
+ + self._get_reports_to_display("xfailed")
+ + self._get_reports_to_display("skipped")
+ + self._get_reports_to_display("error")
+ + self._get_reports_to_display("")
+ )
+ current_location = all_reports[-1].location[0]
+ not_reported = [
+ r for r in all_reports if r.nodeid not in self._timing_nodeids_reported
+ ]
+ tests_in_module = sum(
+ i.location[0] == current_location for i in self._session.items
+ )
+ tests_completed = sum(
+ r.when == "setup"
+ for r in not_reported
+ if r.location[0] == current_location
+ )
+ last_in_module = tests_completed == tests_in_module
+ if self.showlongtestinfo or last_in_module:
+ self._timing_nodeids_reported.update(r.nodeid for r in not_reported)
+ return format_node_duration(
+ sum(r.duration for r in not_reported if isinstance(r, TestReport))
+ )
+ return ""
+ if collected:
+ return f" [{self.reported_progress * 100 // collected:3d}%]"
+ return " [100%]"
+
+ def _write_progress_information_if_past_edge(self) -> None:
+ w = self._width_of_current_line
+ if self._show_progress_info == "count":
+ assert self._session
+ num_tests = self._session.testscollected
+ progress_length = len(f" [{num_tests}/{num_tests}]")
+ elif self._show_progress_info == "times":
+ progress_length = len(" 99h 59m")
+ else:
+ progress_length = len(" [100%]")
+ past_edge = w + progress_length + 1 >= self._screen_width
+ if past_edge:
+ main_color, _ = self._get_main_color()
+ msg = self._get_progress_information_message()
+ self._tw.write(msg + "\n", **{main_color: True})
+
+ def _write_progress_information_filling_space(self) -> None:
+ color, _ = self._get_main_color()
+ msg = self._get_progress_information_message()
+ w = self._width_of_current_line
+ fill = self._tw.fullwidth - w - 1
+ self.write(msg.rjust(fill), flush=True, **{color: True})
+
+ @property
+ def _width_of_current_line(self) -> int:
+ """Return the width of the current line."""
+ return self._tw.width_of_current_line
+
+ def pytest_collection(self) -> None:
+ if self.isatty():
+ if self.config.option.verbose >= 0:
+ self.write("collecting ... ", flush=True, bold=True)
+ elif self.config.option.verbose >= 1:
+ self.write("collecting ... ", flush=True, bold=True)
+
+ def pytest_collectreport(self, report: CollectReport) -> None:
+ if report.failed:
+ self._add_stats("error", [report])
+ elif report.skipped:
+ self._add_stats("skipped", [report])
+ items = [x for x in report.result if isinstance(x, Item)]
+ self._numcollected += len(items)
+ if self.isatty():
+ self.report_collect()
+
+ def report_collect(self, final: bool = False) -> None:
+ if self.config.option.verbose < 0:
+ return
+
+ if not final:
+ # Only write the "collecting" report every `REPORT_COLLECTING_RESOLUTION`.
+ if (
+ self._collect_report_last_write.elapsed().seconds
+ < REPORT_COLLECTING_RESOLUTION
+ ):
+ return
+ self._collect_report_last_write = timing.Instant()
+
+ errors = len(self.stats.get("error", []))
+ skipped = len(self.stats.get("skipped", []))
+ deselected = len(self.stats.get("deselected", []))
+ selected = self._numcollected - deselected
+ line = "collected " if final else "collecting "
+ line += (
+ str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
+ )
+ if errors:
+ line += f" / {errors} error{'s' if errors != 1 else ''}"
+ if deselected:
+ line += f" / {deselected} deselected"
+ if skipped:
+ line += f" / {skipped} skipped"
+ if self._numcollected > selected:
+ line += f" / {selected} selected"
+ if self.isatty():
+ self.rewrite(line, bold=True, erase=True)
+ if final:
+ self.write("\n")
+ else:
+ self.write_line(line)
+
+ @hookimpl(trylast=True)
+ def pytest_sessionstart(self, session: Session) -> None:
+ self._session = session
+ self._session_start = timing.Instant()
+ if not self.showheader:
+ return
+ self.write_sep("=", "test session starts", bold=True)
+ verinfo = platform.python_version()
+ if not self.no_header:
+ msg = f"platform {sys.platform} -- Python {verinfo}"
+ pypy_version_info = getattr(sys, "pypy_version_info", None)
+ if pypy_version_info:
+ verinfo = ".".join(map(str, pypy_version_info[:3]))
+ msg += f"[pypy-{verinfo}-{pypy_version_info[3]}]"
+ msg += f", pytest-{_pytest._version.version}, pluggy-{pluggy.__version__}"
+ if (
+ self.verbosity > 0
+ or self.config.option.debug
+ or getattr(self.config.option, "pastebin", None)
+ ):
+ msg += " -- " + str(sys.executable)
+ self.write_line(msg)
+ lines = self.config.hook.pytest_report_header(
+ config=self.config, start_path=self.startpath
+ )
+ self._write_report_lines_from_hooks(lines)
+
+ def _write_report_lines_from_hooks(
+ self, lines: Sequence[str | Sequence[str]]
+ ) -> None:
+ for line_or_lines in reversed(lines):
+ if isinstance(line_or_lines, str):
+ self.write_line(line_or_lines)
+ else:
+ for line in line_or_lines:
+ self.write_line(line)
+
+ def pytest_report_header(self, config: Config) -> list[str]:
+ result = [f"rootdir: {config.rootpath}"]
+
+ if config.inipath:
+ warning = ""
+ if config._ignored_config_files:
+ warning = f" (WARNING: ignoring pytest config in {', '.join(config._ignored_config_files)}!)"
+ result.append(
+ "configfile: " + bestrelpath(config.rootpath, config.inipath) + warning
+ )
+
+ if config.args_source == Config.ArgsSource.TESTPATHS:
+ testpaths: list[str] = config.getini("testpaths")
+ result.append("testpaths: {}".format(", ".join(testpaths)))
+
+ plugininfo = config.pluginmanager.list_plugin_distinfo()
+ if plugininfo:
+ result.append(
+ "plugins: {}".format(", ".join(_plugin_nameversions(plugininfo)))
+ )
+ return result
+
+ def pytest_collection_finish(self, session: Session) -> None:
+ self.report_collect(True)
+
+ lines = self.config.hook.pytest_report_collectionfinish(
+ config=self.config,
+ start_path=self.startpath,
+ items=session.items,
+ )
+ self._write_report_lines_from_hooks(lines)
+
+ if self.config.getoption("collectonly"):
+ if session.items:
+ if self.config.option.verbose > -1:
+ self._tw.line("")
+ self._printcollecteditems(session.items)
+
+ failed = self.stats.get("failed")
+ if failed:
+ self._tw.sep("!", "collection failures")
+ for rep in failed:
+ rep.toterminal(self._tw)
+
+ def _printcollecteditems(self, items: Sequence[Item]) -> None:
+ test_cases_verbosity = self.config.get_verbosity(Config.VERBOSITY_TEST_CASES)
+ if test_cases_verbosity < 0:
+ if test_cases_verbosity < -1:
+ counts = Counter(item.nodeid.split("::", 1)[0] for item in items)
+ for name, count in sorted(counts.items()):
+ self._tw.line(f"{name}: {count}")
+ else:
+ for item in items:
+ self._tw.line(item.nodeid)
+ return
+ stack: list[Node] = []
+ indent = ""
+ for item in items:
+ needed_collectors = item.listchain()[1:] # strip root node
+ while stack:
+ if stack == needed_collectors[: len(stack)]:
+ break
+ stack.pop()
+ for col in needed_collectors[len(stack) :]:
+ stack.append(col)
+ indent = (len(stack) - 1) * " "
+ self._tw.line(f"{indent}{col}")
+ if test_cases_verbosity >= 1:
+ obj = getattr(col, "obj", None)
+ doc = inspect.getdoc(obj) if obj else None
+ if doc:
+ for line in doc.splitlines():
+ self._tw.line("{}{}".format(indent + " ", line))
+
+ @hookimpl(wrapper=True)
+ def pytest_sessionfinish(
+ self, session: Session, exitstatus: int | ExitCode
+ ) -> Generator[None]:
+ result = yield
+ self._tw.line("")
+ summary_exit_codes = (
+ ExitCode.OK,
+ ExitCode.TESTS_FAILED,
+ ExitCode.INTERRUPTED,
+ ExitCode.USAGE_ERROR,
+ ExitCode.NO_TESTS_COLLECTED,
+ ExitCode.MAX_WARNINGS_ERROR,
+ )
+ if exitstatus in summary_exit_codes and not self.no_summary:
+ self.config.hook.pytest_terminal_summary(
+ terminalreporter=self, exitstatus=exitstatus, config=self.config
+ )
+ # Check --max-warnings threshold after all warnings have been collected.
+ max_warnings = self._get_max_warnings()
+ if max_warnings is not None and session.exitstatus == ExitCode.OK:
+ warning_count = len(self.stats.get("warnings", []))
+ if warning_count > max_warnings:
+ session.exitstatus = ExitCode.MAX_WARNINGS_ERROR
+ self.write_line(
+ "Tests pass, but maximum allowed warnings exceeded: "
+ f"{warning_count} > {max_warnings}",
+ red=True,
+ )
+ if session.shouldfail:
+ self.write_sep("!", str(session.shouldfail), red=True)
+ if exitstatus == ExitCode.INTERRUPTED:
+ self._report_keyboardinterrupt()
+ self._keyboardinterrupt_memo = None
+ elif session.shouldstop:
+ self.write_sep("!", str(session.shouldstop), red=True)
+ self.summary_stats()
+ return result
+
+ @hookimpl(wrapper=True)
+ def pytest_terminal_summary(self) -> Generator[None]:
+ self.summary_errors()
+ self.summary_failures()
+ self.summary_xfailures()
+ self.summary_warnings()
+ self.summary_passes()
+ self.summary_xpasses()
+ try:
+ return (yield)
+ finally:
+ self.short_test_summary()
+ # Display any extra warnings from teardown here (if any).
+ self.summary_warnings()
+
+ def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None:
+ self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
+
+ def pytest_unconfigure(self) -> None:
+ if self._keyboardinterrupt_memo is not None:
+ self._report_keyboardinterrupt()
+
+ def _report_keyboardinterrupt(self) -> None:
+ excrepr = self._keyboardinterrupt_memo
+ assert excrepr is not None
+ assert excrepr.reprcrash is not None
+ msg = excrepr.reprcrash.message
+ self.write_sep("!", msg)
+ if "KeyboardInterrupt" in msg:
+ if self.config.option.fulltrace:
+ excrepr.toterminal(self._tw)
+ else:
+ excrepr.reprcrash.toterminal(self._tw)
+ self._tw.line(
+ "(to show a full traceback on KeyboardInterrupt use --full-trace)",
+ yellow=True,
+ )
+
+ def _locationline(
+ self, nodeid: str, fspath: str, lineno: int | None, domain: str
+ ) -> str:
+ def mkrel(nodeid: str) -> str:
+ line = self.config.cwd_relative_nodeid(nodeid)
+ if domain and line.endswith(domain):
+ line = line[: -len(domain)]
+ values = domain.split("[")
+ values[0] = values[0].replace(".", "::") # don't replace '.' in params
+ line += "[".join(values)
+ return line
+
+ # fspath comes from testid which has a "/"-normalized path.
+ if fspath:
+ res = mkrel(nodeid)
+ if self.verbosity >= 2 and (
+ nodeid.split("::", maxsplit=1)[0] != nodes.norm_sep(fspath)
+ ):
+ res += " <- " + bestrelpath(self.startpath, Path(fspath))
+ else:
+ res = "[location]"
+ return res + " "
+
+ def _getfailureheadline(self, rep):
+ head_line = rep.head_line
+ if head_line:
+ return head_line
+ return "test session" # XXX?
+
+ def _getcrashline(self, rep):
+ try:
+ return str(rep.longrepr.reprcrash)
+ except AttributeError:
+ try:
+ return str(rep.longrepr)[:50]
+ except AttributeError:
+ return ""
+
+ def _get_max_warnings(self) -> int | None:
+ """Return the max_warnings threshold, from CLI or INI, or None if unset."""
+ value = self.config.option.max_warnings
+ if value is not None:
+ return int(value)
+ ini_value = self.config.getini("max_warnings")
+ if ini_value:
+ return int(ini_value)
+ return None
+
+ #
+ # Summaries for sessionfinish.
+ #
+ def getreports(self, name: str):
+ return [x for x in self.stats.get(name, ()) if not hasattr(x, "_pdbshown")]
+
+ def summary_warnings(self) -> None:
+ if self.hasopt("w"):
+ all_warnings: list[WarningReport] | None = self.stats.get("warnings")
+ if not all_warnings:
+ return
+
+ final = self._already_displayed_warnings is not None
+ if final:
+ warning_reports = all_warnings[self._already_displayed_warnings :]
+ else:
+ warning_reports = all_warnings
+ self._already_displayed_warnings = len(warning_reports)
+ if not warning_reports:
+ return
+
+ reports_grouped_by_message: dict[str, list[WarningReport]] = {}
+ for wr in warning_reports:
+ reports_grouped_by_message.setdefault(wr.message, []).append(wr)
+
+ def collapsed_location_report(reports: list[WarningReport]) -> str:
+ locations = []
+ for w in reports:
+ location = w.get_location(self.config)
+ if location:
+ locations.append(location)
+
+ if len(locations) < 10:
+ return "\n".join(map(str, locations))
+
+ counts_by_filename = Counter(
+ str(loc).split("::", 1)[0] for loc in locations
+ )
+ return "\n".join(
+ "{}: {} warning{}".format(k, v, "s" if v > 1 else "")
+ for k, v in counts_by_filename.items()
+ )
+
+ title = "warnings summary (final)" if final else "warnings summary"
+ self.write_sep("=", title, yellow=True, bold=False)
+ for message, message_reports in reports_grouped_by_message.items():
+ maybe_location = collapsed_location_report(message_reports)
+ if maybe_location:
+ self._tw.line(maybe_location)
+ lines = message.splitlines()
+ indented = "\n".join(" " + x for x in lines)
+ message = indented.rstrip()
+ else:
+ message = message.rstrip()
+ self._tw.line(message)
+ self._tw.line()
+ self._tw.line(
+ "-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html"
+ )
+
+ def summary_passes(self) -> None:
+ self.summary_passes_combined("passed", "PASSES", "P")
+
+ def summary_xpasses(self) -> None:
+ self.summary_passes_combined("xpassed", "XPASSES", "X")
+
+ def summary_passes_combined(
+ self, which_reports: str, sep_title: str, needed_opt: str
+ ) -> None:
+ if self.config.option.tbstyle != "no":
+ if self.hasopt(needed_opt):
+ reports: list[TestReport] = self.getreports(which_reports)
+ if not reports:
+ return
+ self.write_sep("=", sep_title)
+ for rep in reports:
+ if rep.sections:
+ msg = self._getfailureheadline(rep)
+ self.write_sep("_", msg, green=True, bold=True)
+ self._outrep_summary(rep)
+ self._handle_teardown_sections(rep.nodeid)
+
+ def _get_teardown_reports(self, nodeid: str) -> list[TestReport]:
+ reports = self.getreports("")
+ return [
+ report
+ for report in reports
+ if report.when == "teardown" and report.nodeid == nodeid
+ ]
+
+ def _handle_teardown_sections(self, nodeid: str) -> None:
+ for report in self._get_teardown_reports(nodeid):
+ self.print_teardown_sections(report)
+
+ def print_teardown_sections(self, rep: TestReport) -> None:
+ showcapture = self.config.option.showcapture
+ if showcapture == "no":
+ return
+ for secname, content in rep.sections:
+ if showcapture != "all" and showcapture not in secname:
+ continue
+ if "teardown" in secname:
+ self._tw.sep("-", secname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ self._tw.line(content)
+
+ def summary_failures(self) -> None:
+ style = self.config.option.tbstyle
+ self.summary_failures_combined("failed", "FAILURES", style=style)
+
+ def summary_xfailures(self) -> None:
+ show_tb = self.config.option.xfail_tb
+ style = self.config.option.tbstyle if show_tb else "no"
+ self.summary_failures_combined("xfailed", "XFAILURES", style=style)
+
+ def summary_failures_combined(
+ self,
+ which_reports: str,
+ sep_title: str,
+ *,
+ style: str,
+ needed_opt: str | None = None,
+ ) -> None:
+ if style != "no":
+ if not needed_opt or self.hasopt(needed_opt):
+ reports: list[BaseReport] = self.getreports(which_reports)
+ if not reports:
+ return
+ self.write_sep("=", sep_title)
+ if style == "line":
+ for rep in reports:
+ line = self._getcrashline(rep)
+ self._outrep_summary(rep)
+ self.write_line(line)
+ else:
+ for rep in reports:
+ msg = self._getfailureheadline(rep)
+ self.write_sep("_", msg, red=True, bold=True)
+ self._outrep_summary(rep)
+ self._handle_teardown_sections(rep.nodeid)
+
+ def summary_errors(self) -> None:
+ if self.config.option.tbstyle != "no":
+ reports: list[BaseReport] = self.getreports("error")
+ if not reports:
+ return
+ self.write_sep("=", "ERRORS")
+ for rep in self.stats["error"]:
+ msg = self._getfailureheadline(rep)
+ if rep.when == "collect":
+ msg = "ERROR collecting " + msg
+ else:
+ msg = f"ERROR at {rep.when} of {msg}"
+ self.write_sep("_", msg, red=True, bold=True)
+ self._outrep_summary(rep)
+
+ def _outrep_summary(self, rep: BaseReport) -> None:
+ rep.toterminal(self._tw)
+ showcapture = self.config.option.showcapture
+ if showcapture == "no":
+ return
+ for secname, content in rep.sections:
+ if showcapture != "all" and showcapture not in secname:
+ continue
+ self._tw.sep("-", secname)
+ if content[-1:] == "\n":
+ content = content[:-1]
+ self._tw.line(content)
+
+ def summary_stats(self) -> None:
+ if self.verbosity < -1:
+ return
+
+ session_duration = self._session_start.elapsed()
+ (parts, main_color) = self.build_summary_stats_line()
+ line_parts = []
+
+ display_sep = self.verbosity >= 0
+ if display_sep:
+ fullwidth = self._tw.fullwidth
+ for text, markup in parts:
+ with_markup = self._tw.markup(text, **markup)
+ if display_sep:
+ fullwidth += len(with_markup) - len(text)
+ line_parts.append(with_markup)
+ msg = ", ".join(line_parts)
+
+ main_markup = {main_color: True}
+ duration = f" in {format_session_duration(session_duration.seconds)}"
+ duration_with_markup = self._tw.markup(duration, **main_markup)
+ if display_sep:
+ fullwidth += len(duration_with_markup) - len(duration)
+ msg += duration_with_markup
+
+ if display_sep:
+ markup_for_end_sep = self._tw.markup("", **main_markup)
+ if markup_for_end_sep.endswith("\x1b[0m"):
+ markup_for_end_sep = markup_for_end_sep[:-4]
+ fullwidth += len(markup_for_end_sep)
+ msg += markup_for_end_sep
+
+ if display_sep:
+ self.write_sep("=", msg, fullwidth=fullwidth, **main_markup)
+ else:
+ self.write_line(msg, **main_markup)
+
+ def short_test_summary(self) -> None:
+ if not self.reportchars:
+ return
+
+ def show_simple(lines: list[str], *, stat: str) -> None:
+ failed = self.stats.get(stat, [])
+ if not failed:
+ return
+ config = self.config
+ for rep in failed:
+ color = _color_for_type.get(stat, _color_for_type_default)
+ line = _get_line_with_reprcrash_message(
+ config, rep, self._tw, {color: True}
+ )
+ lines.append(line)
+
+ def show_xfailed(lines: list[str]) -> None:
+ xfailed = self.stats.get("xfailed", [])
+ for rep in xfailed:
+ verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
+ self.config, {_color_for_type["warnings"]: True}
+ )
+ markup_word = self._tw.markup(verbose_word, **verbose_markup)
+ nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
+ line = f"{markup_word} {nodeid}"
+ reason = rep.wasxfail
+ if reason:
+ line += " - " + str(reason)
+
+ lines.append(line)
+
+ def show_xpassed(lines: list[str]) -> None:
+ xpassed = self.stats.get("xpassed", [])
+ for rep in xpassed:
+ verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
+ self.config, {_color_for_type["warnings"]: True}
+ )
+ markup_word = self._tw.markup(verbose_word, **verbose_markup)
+ nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
+ line = f"{markup_word} {nodeid}"
+ reason = rep.wasxfail
+ if reason:
+ line += " - " + str(reason)
+ lines.append(line)
+
+ def show_skipped_folded(lines: list[str]) -> None:
+ skipped: list[CollectReport] = self.stats.get("skipped", [])
+ fskips = _folded_skips(self.startpath, skipped) if skipped else []
+ if not fskips:
+ return
+ verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup(
+ self.config, {_color_for_type["warnings"]: True}
+ )
+ markup_word = self._tw.markup(verbose_word, **verbose_markup)
+ prefix = "Skipped: "
+ for num, fspath, lineno, reason in fskips:
+ if reason.startswith(prefix):
+ reason = reason[len(prefix) :]
+ if lineno is not None:
+ lines.append(f"{markup_word} [{num}] {fspath}:{lineno}: {reason}")
+ else:
+ lines.append(f"{markup_word} [{num}] {fspath}: {reason}")
+
+ def show_skipped_unfolded(lines: list[str]) -> None:
+ skipped: list[CollectReport] = self.stats.get("skipped", [])
+
+ for rep in skipped:
+ assert rep.longrepr is not None
+ assert isinstance(rep.longrepr, tuple), (rep, rep.longrepr)
+ assert len(rep.longrepr) == 3, (rep, rep.longrepr)
+
+ verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
+ self.config, {_color_for_type["warnings"]: True}
+ )
+ markup_word = self._tw.markup(verbose_word, **verbose_markup)
+ nodeid = _get_node_id_with_markup(self._tw, self.config, rep)
+ line = f"{markup_word} {nodeid}"
+ reason = rep.longrepr[2]
+ if reason:
+ line += " - " + str(reason)
+ lines.append(line)
+
+ def show_skipped(lines: list[str]) -> None:
+ if self.foldskipped:
+ show_skipped_folded(lines)
+ else:
+ show_skipped_unfolded(lines)
+
+ REPORTCHAR_ACTIONS: Mapping[str, Callable[[list[str]], None]] = {
+ "x": show_xfailed,
+ "X": show_xpassed,
+ "f": partial(show_simple, stat="failed"),
+ "s": show_skipped,
+ "p": partial(show_simple, stat="passed"),
+ "E": partial(show_simple, stat="error"),
+ }
+
+ lines: list[str] = []
+ for char in self.reportchars:
+ action = REPORTCHAR_ACTIONS.get(char)
+ if action: # skipping e.g. "P" (passed with output) here.
+ action(lines)
+
+ if lines:
+ self.write_sep("=", "short test summary info", cyan=True, bold=True)
+ for line in lines:
+ self.write_line(line)
+
+ def _get_main_color(self) -> tuple[str, list[str]]:
+ if self._main_color is None or self._known_types is None or self._is_last_item:
+ self._set_main_color()
+ assert self._main_color
+ assert self._known_types
+ return self._main_color, self._known_types
+
+ def _determine_main_color(self, unknown_type_seen: bool) -> str:
+ stats = self.stats
+ if "failed" in stats or "error" in stats:
+ main_color = "red"
+ elif "warnings" in stats or "xpassed" in stats or unknown_type_seen:
+ main_color = "yellow"
+ elif "passed" in stats or not self._is_last_item:
+ main_color = "green"
+ else:
+ main_color = "yellow"
+ return main_color
+
+ def _set_main_color(self) -> None:
+ unknown_types: list[str] = []
+ for found_type in self.stats:
+ if found_type: # setup/teardown reports have an empty key, ignore them
+ if found_type not in KNOWN_TYPES and found_type not in unknown_types:
+ unknown_types.append(found_type)
+ self._known_types = list(KNOWN_TYPES) + unknown_types
+ self._main_color = self._determine_main_color(bool(unknown_types))
+
+ def build_summary_stats_line(self) -> tuple[list[tuple[str, dict[str, bool]]], str]:
+ """
+ Build the parts used in the last summary stats line.
+
+ The summary stats line is the line shown at the end, "=== 12 passed, 2 errors in Xs===".
+
+ This function builds a list of the "parts" that make up for the text in that line, in
+ the example above it would be::
+
+ [
+ ("12 passed", {"green": True}),
+ ("2 errors", {"red": True}
+ ]
+
+ That last dict for each line is a "markup dictionary", used by TerminalWriter to
+ color output.
+
+ The final color of the line is also determined by this function, and is the second
+ element of the returned tuple.
+ """
+ if self.config.getoption("collectonly"):
+ return self._build_collect_only_summary_stats_line()
+ else:
+ return self._build_normal_summary_stats_line()
+
+ def _get_reports_to_display(self, key: str) -> list[Any]:
+ """Get test/collection reports for the given status key, such as `passed` or `error`."""
+ reports = self.stats.get(key, [])
+ return [x for x in reports if getattr(x, "count_towards_summary", True)]
+
+ def _build_normal_summary_stats_line(
+ self,
+ ) -> tuple[list[tuple[str, dict[str, bool]]], str]:
+ main_color, known_types = self._get_main_color()
+ parts = []
+
+ for key in known_types:
+ reports = self._get_reports_to_display(key)
+ if reports:
+ count = len(reports)
+ color = _color_for_type.get(key, _color_for_type_default)
+ markup = {color: True, "bold": color == main_color}
+ parts.append(("%d %s" % pluralize(count, key), markup)) # noqa: UP031
+
+ if not parts:
+ parts = [("no tests ran", {_color_for_type_default: True})]
+
+ return parts, main_color
+
+ def _build_collect_only_summary_stats_line(
+ self,
+ ) -> tuple[list[tuple[str, dict[str, bool]]], str]:
+ deselected = len(self._get_reports_to_display("deselected"))
+ errors = len(self._get_reports_to_display("error"))
+
+ if self._numcollected == 0:
+ parts = [("no tests collected", {"yellow": True})]
+ main_color = "yellow"
+
+ elif deselected == 0:
+ main_color = "green"
+ collected_output = "%d %s collected" % pluralize(self._numcollected, "test") # noqa: UP031
+ parts = [(collected_output, {main_color: True})]
+ else:
+ all_tests_were_deselected = self._numcollected == deselected
+ if all_tests_were_deselected:
+ main_color = "yellow"
+ collected_output = f"no tests collected ({deselected} deselected)"
+ else:
+ main_color = "green"
+ selected = self._numcollected - deselected
+ collected_output = f"{selected}/{self._numcollected} tests collected ({deselected} deselected)"
+
+ parts = [(collected_output, {main_color: True})]
+
+ if errors:
+ main_color = _color_for_type["error"]
+ parts += [("%d %s" % pluralize(errors, "error"), {main_color: True})] # noqa: UP031
+
+ return parts, main_color
+
+
+def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport):
+ nodeid = config.cwd_relative_nodeid(rep.nodeid)
+ path, *parts = nodeid.split("::")
+ if parts:
+ parts_markup = tw.markup("::".join(parts), bold=True)
+ return path + "::" + parts_markup
+ else:
+ return path
+
+
+def _format_trimmed(format: str, msg: str, available_width: int) -> str | None:
+ """Format msg into format, ellipsizing it if doesn't fit in available_width.
+
+ Returns None if even the ellipsis can't fit.
+ """
+ # Only use the first line.
+ i = msg.find("\n")
+ if i != -1:
+ msg = msg[:i]
+
+ ellipsis = "..."
+ format_width = wcswidth(format.format(""))
+ if format_width + len(ellipsis) > available_width:
+ return None
+
+ if format_width + wcswidth(msg) > available_width:
+ available_width -= len(ellipsis)
+ msg = msg[:available_width]
+ while format_width + wcswidth(msg) > available_width:
+ msg = msg[:-1]
+ msg += ellipsis
+
+ return format.format(msg)
+
+
+def _get_line_with_reprcrash_message(
+ config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool]
+) -> str:
+ """Get summary line for a report, trying to add reprcrash message."""
+ verbose_word, verbose_markup = rep._get_verbose_word_with_markup(
+ config, word_markup
+ )
+ word = tw.markup(verbose_word, **verbose_markup)
+ node = _get_node_id_with_markup(tw, config, rep)
+
+ line = f"{word} {node}"
+ line_width = wcswidth(line)
+
+ msg: str | None
+ try:
+ if isinstance(rep.longrepr, str):
+ msg = rep.longrepr
+ else:
+ # Type ignored intentionally -- possible AttributeError expected.
+ msg = rep.longrepr.reprcrash.message # type: ignore[union-attr]
+ except AttributeError:
+ pass
+ else:
+ if (
+ running_on_ci() or config.option.verbose >= 2
+ ) and not config.option.force_short_summary:
+ msg = f" - {msg}"
+ else:
+ available_width = tw.fullwidth - line_width
+ msg = _format_trimmed(" - {}", msg, available_width)
+ if msg is not None:
+ line += msg
+
+ return line
+
+
+def _folded_skips(
+ startpath: Path,
+ skipped: Sequence[CollectReport],
+) -> list[tuple[int, str, int | None, str]]:
+ d: dict[tuple[str, int | None, str], list[CollectReport]] = {}
+ for event in skipped:
+ assert event.longrepr is not None
+ assert isinstance(event.longrepr, tuple), (event, event.longrepr)
+ assert len(event.longrepr) == 3, (event, event.longrepr)
+ fspath, lineno, reason = event.longrepr
+ # For consistency, report all fspaths in relative form.
+ fspath = bestrelpath(startpath, Path(fspath))
+ keywords = getattr(event, "keywords", {})
+ # Folding reports with global pytestmark variable.
+ # This is a workaround, because for now we cannot identify the scope of a skip marker
+ # TODO: Revisit after marks scope would be fixed.
+ if (
+ event.when == "setup"
+ and "skip" in keywords
+ and "pytestmark" not in keywords
+ ):
+ key: tuple[str, int | None, str] = (fspath, None, reason)
+ else:
+ key = (fspath, lineno, reason)
+ d.setdefault(key, []).append(event)
+ values: list[tuple[int, str, int | None, str]] = []
+ for key, events in d.items():
+ values.append((len(events), *key))
+ return values
+
+
+_color_for_type = {
+ "failed": "red",
+ "error": "red",
+ "warnings": "yellow",
+ "passed": "green",
+ "subtests passed": "green",
+ "subtests failed": "red",
+}
+_color_for_type_default = "yellow"
+
+
+def pluralize(count: int, noun: str) -> tuple[int, str]:
+ # No need to pluralize words such as `failed` or `passed`.
+ if noun not in ["error", "warnings", "test"]:
+ return count, noun
+
+ # The `warnings` key is plural. To avoid API breakage, we keep it that way but
+ # set it to singular here so we can determine plurality in the same way as we do
+ # for `error`.
+ noun = noun.replace("warnings", "warning")
+
+ return count, noun + "s" if count != 1 else noun
+
+
+def _plugin_nameversions(plugininfo) -> list[str]:
+ values: list[str] = []
+ for plugin, dist in plugininfo:
+ # Gets us name and version!
+ name = f"{dist.project_name}-{dist.version}"
+ # Questionable convenience, but it keeps things short.
+ if name.startswith("pytest-"):
+ name = name[7:]
+ # We decided to print python package names they can have more than one plugin.
+ if name not in values:
+ values.append(name)
+ return values
+
+
+def format_session_duration(seconds: float) -> str:
+ """Format the given seconds in a human readable manner to show in the final summary."""
+ if seconds < 60:
+ return f"{seconds:.2f}s"
+ else:
+ dt = datetime.timedelta(seconds=int(seconds))
+ return f"{seconds:.2f}s ({dt})"
+
+
+def format_node_duration(seconds: float) -> str:
+ """Format the given seconds in a human readable manner to show in the test progress."""
+ # The formatting is designed to be compact and readable, with at most 7 characters
+ # for durations below 100 hours.
+ if seconds < 0.00001:
+ return f" {seconds * 1000000:.3f}us"
+ if seconds < 0.0001:
+ return f" {seconds * 1000000:.2f}us"
+ if seconds < 0.001:
+ return f" {seconds * 1000000:.1f}us"
+ if seconds < 0.01:
+ return f" {seconds * 1000:.3f}ms"
+ if seconds < 0.1:
+ return f" {seconds * 1000:.2f}ms"
+ if seconds < 1:
+ return f" {seconds * 1000:.1f}ms"
+ if seconds < 60:
+ return f" {seconds:.3f}s"
+ if seconds < 3600:
+ return f" {seconds // 60:.0f}m {seconds % 60:.0f}s"
+ return f" {seconds // 3600:.0f}h {(seconds % 3600) // 60:.0f}m"
+
+
+def _get_raw_skip_reason(report: TestReport) -> str:
+ """Get the reason string of a skip/xfail/xpass test report.
+
+ The string is just the part given by the user.
+ """
+ if hasattr(report, "wasxfail"):
+ reason = report.wasxfail
+ if reason.startswith("reason: "):
+ reason = reason[len("reason: ") :]
+ return reason
+ else:
+ assert report.skipped
+ assert isinstance(report.longrepr, tuple)
+ _, _, reason = report.longrepr
+ if reason.startswith("Skipped: "):
+ reason = reason[len("Skipped: ") :]
+ elif reason == "Skipped":
+ reason = ""
+ return reason
+
+
+class TerminalProgressPlugin:
+ """Terminal progress reporting plugin using OSC 9;4 ANSI sequences.
+
+ Emits OSC 9;4 sequences to indicate test progress to terminal
+ tabs/windows/etc.
+
+ Not all terminal emulators support this feature.
+
+ Ref: https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
+ """
+
+ def __init__(self, tr: TerminalReporter) -> None:
+ self._tr = tr
+ self._session: Session | None = None
+ self._has_failures = False
+
+ def _emit_progress(
+ self,
+ state: Literal["remove", "normal", "error", "indeterminate", "paused"],
+ progress: int | None = None,
+ ) -> None:
+ """Emit OSC 9;4 sequence for indicating progress to the terminal.
+
+ :param state:
+ Progress state to set.
+ :param progress:
+ Progress value 0-100. Required for "normal", optional for "error"
+ and "paused", otherwise ignored.
+ """
+ assert progress is None or 0 <= progress <= 100
+
+ # OSC 9;4 sequence: ESC ] 9 ; 4 ; state ; progress ST
+ # ST can be ESC \ or BEL. ESC \ seems better supported.
+ match state:
+ case "remove":
+ sequence = "\x1b]9;4;0;\x1b\\"
+ case "normal":
+ assert progress is not None
+ sequence = f"\x1b]9;4;1;{progress}\x1b\\"
+ case "error":
+ if progress is not None:
+ sequence = f"\x1b]9;4;2;{progress}\x1b\\"
+ else:
+ sequence = "\x1b]9;4;2;\x1b\\"
+ case "indeterminate":
+ sequence = "\x1b]9;4;3;\x1b\\"
+ case "paused":
+ if progress is not None:
+ sequence = f"\x1b]9;4;4;{progress}\x1b\\"
+ else:
+ sequence = "\x1b]9;4;4;\x1b\\"
+
+ self._tr.write_raw(sequence, flush=True)
+
+ @hookimpl
+ def pytest_sessionstart(self, session: Session) -> None:
+ self._session = session
+ # Show indeterminate progress during collection.
+ self._emit_progress("indeterminate")
+
+ @hookimpl
+ def pytest_collection_finish(self) -> None:
+ assert self._session is not None
+ if self._session.testscollected > 0:
+ # Switch from indeterminate to 0% progress.
+ self._emit_progress("normal", 0)
+
+ @hookimpl
+ def pytest_runtest_logreport(self, report: TestReport) -> None:
+ if report.failed:
+ self._has_failures = True
+
+ # Let's consider the "call" phase for progress.
+ if report.when != "call":
+ return
+
+ # Calculate and emit progress.
+ assert self._session is not None
+ collected = self._session.testscollected
+ if collected > 0:
+ reported = self._tr.reported_progress
+ progress = min(reported * 100 // collected, 100)
+ self._emit_progress("error" if self._has_failures else "normal", progress)
+
+ @hookimpl
+ def pytest_sessionfinish(self) -> None:
+ self._emit_progress("remove")
diff --git a/venv/lib/python3.11/site-packages/_pytest/terminalprogress.py b/venv/lib/python3.11/site-packages/_pytest/terminalprogress.py
new file mode 100644
index 000000000..287f0d569
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/terminalprogress.py
@@ -0,0 +1,30 @@
+# A plugin to register the TerminalProgressPlugin plugin.
+#
+# This plugin is not loaded by default due to compatibility issues (#13896),
+# but can be enabled in one of these ways:
+# - The terminal plugin enables it in a few cases where it's safe, and not
+# blocked by the user (using e.g. `-p no:terminalprogress`).
+# - The user explicitly requests it, e.g. using `-p terminalprogress`.
+#
+# In a few years, if it's safe, we can consider enabling it by default. Then,
+# this file will become unnecessary and can be inlined into terminal.py.
+
+from __future__ import annotations
+
+import os
+
+from _pytest.config import Config
+from _pytest.config import hookimpl
+from _pytest.terminal import TerminalProgressPlugin
+from _pytest.terminal import TerminalReporter
+
+
+@hookimpl(trylast=True)
+def pytest_configure(config: Config) -> None:
+ reporter: TerminalReporter | None = config.pluginmanager.get_plugin(
+ "terminalreporter"
+ )
+
+ if reporter is not None and reporter.isatty() and os.environ.get("TERM") != "dumb":
+ plugin = TerminalProgressPlugin(reporter)
+ config.pluginmanager.register(plugin, name="terminalprogress-plugin")
diff --git a/venv/lib/python3.11/site-packages/_pytest/threadexception.py b/venv/lib/python3.11/site-packages/_pytest/threadexception.py
new file mode 100644
index 000000000..eb57783be
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/threadexception.py
@@ -0,0 +1,152 @@
+from __future__ import annotations
+
+import collections
+from collections.abc import Callable
+import functools
+import sys
+import threading
+import traceback
+from typing import NamedTuple
+from typing import TYPE_CHECKING
+import warnings
+
+from _pytest.config import Config
+from _pytest.nodes import Item
+from _pytest.stash import StashKey
+from _pytest.tracemalloc import tracemalloc_message
+import pytest
+
+
+if TYPE_CHECKING:
+ pass
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
+
+class ThreadExceptionMeta(NamedTuple):
+ msg: str
+ cause_msg: str
+ exc_value: BaseException | None
+
+
+thread_exceptions: StashKey[collections.deque[ThreadExceptionMeta | BaseException]] = (
+ StashKey()
+)
+
+
+def collect_thread_exception(config: Config) -> None:
+ pop_thread_exception = config.stash[thread_exceptions].pop
+ errors: list[pytest.PytestUnhandledThreadExceptionWarning | RuntimeError] = []
+ meta = None
+ hook_error = None
+ try:
+ while True:
+ try:
+ meta = pop_thread_exception()
+ except IndexError:
+ break
+
+ if isinstance(meta, BaseException):
+ hook_error = RuntimeError("Failed to process thread exception")
+ hook_error.__cause__ = meta
+ errors.append(hook_error)
+ continue
+
+ msg = meta.msg
+ try:
+ warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))
+ except pytest.PytestUnhandledThreadExceptionWarning as e:
+ # This except happens when the warning is treated as an error (e.g. `-Werror`).
+ if meta.exc_value is not None:
+ # Exceptions have a better way to show the traceback, but
+ # warnings do not, so hide the traceback from the msg and
+ # set the cause so the traceback shows up in the right place.
+ e.args = (meta.cause_msg,)
+ e.__cause__ = meta.exc_value
+ errors.append(e)
+
+ if len(errors) == 1:
+ raise errors[0]
+ if errors:
+ raise ExceptionGroup("multiple thread exception warnings", errors)
+ finally:
+ del errors, meta, hook_error
+
+
+def cleanup(
+ *, config: Config, prev_hook: Callable[[threading.ExceptHookArgs], object]
+) -> None:
+ try:
+ try:
+ # We don't join threads here, so exceptions raised from any
+ # threads still running by the time _threading_atexits joins them
+ # do not get captured (see #13027).
+ collect_thread_exception(config)
+ finally:
+ threading.excepthook = prev_hook
+ finally:
+ del config.stash[thread_exceptions]
+
+
+def thread_exception_hook(
+ args: threading.ExceptHookArgs,
+ /,
+ *,
+ append: Callable[[ThreadExceptionMeta | BaseException], object],
+) -> None:
+ try:
+ # we need to compute these strings here as they might change after
+ # the excepthook finishes and before the metadata object is
+ # collected by a pytest hook
+ thread_name = "" if args.thread is None else args.thread.name
+ summary = f"Exception in thread {thread_name}"
+ traceback_message = "\n\n" + "".join(
+ traceback.format_exception(
+ args.exc_type,
+ args.exc_value,
+ args.exc_traceback,
+ )
+ )
+ tracemalloc_tb = "\n" + tracemalloc_message(args.thread)
+ msg = summary + traceback_message + tracemalloc_tb
+ cause_msg = summary + tracemalloc_tb
+
+ append(
+ ThreadExceptionMeta(
+ # Compute these strings here as they might change later
+ msg=msg,
+ cause_msg=cause_msg,
+ exc_value=args.exc_value,
+ )
+ )
+ except BaseException as e:
+ append(e)
+ # Raising this will cause the exception to be logged twice, once in our
+ # collect_thread_exception and once by sys.excepthook
+ # which is fine - this should never happen anyway and if it does
+ # it should probably be reported as a pytest bug.
+ raise
+
+
+def pytest_configure(config: Config) -> None:
+ prev_hook = threading.excepthook
+ deque: collections.deque[ThreadExceptionMeta | BaseException] = collections.deque()
+ config.stash[thread_exceptions] = deque
+ config.add_cleanup(functools.partial(cleanup, config=config, prev_hook=prev_hook))
+ threading.excepthook = functools.partial(thread_exception_hook, append=deque.append)
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_runtest_setup(item: Item) -> None:
+ collect_thread_exception(item.config)
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_runtest_call(item: Item) -> None:
+ collect_thread_exception(item.config)
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_runtest_teardown(item: Item) -> None:
+ collect_thread_exception(item.config)
diff --git a/venv/lib/python3.11/site-packages/_pytest/timing.py b/venv/lib/python3.11/site-packages/_pytest/timing.py
new file mode 100644
index 000000000..639bd99bd
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/timing.py
@@ -0,0 +1,97 @@
+"""Indirection for time functions.
+
+We intentionally grab some "time" functions internally to avoid tests mocking "time" to affect
+pytest runtime information (issue #185).
+
+Fixture "mock_timing" also interacts with this module for pytest's own tests.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+from datetime import datetime
+from datetime import timezone
+from time import perf_counter
+from time import sleep
+from time import time
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from pytest import MonkeyPatch
+
+
+@dataclasses.dataclass(frozen=True)
+class Instant:
+ """
+ Represents an instant in time, used to both get the timestamp value and to measure
+ the duration of a time span.
+
+ Inspired by Rust's `std::time::Instant`.
+ """
+
+ # Creation time of this instant, using time.time(), to measure actual time.
+ # Note: using a `lambda` to correctly get the mocked time via `MockTiming`.
+ # pylint: disable-next=lambda-assignment
+ time: float = dataclasses.field(default_factory=lambda: time(), init=False) # noqa: PLW0108
+
+ # Performance counter tick of the instant, used to measure precise elapsed time.
+ # Note: using a `lambda` to correctly get the mocked time via `MockTiming`.
+ perf_count: float = dataclasses.field(
+ default_factory=lambda: perf_counter(), # noqa: PLW0108
+ init=False,
+ )
+
+ def elapsed(self) -> Duration:
+ """Measure the duration since `Instant` was created."""
+ return Duration(start=self, stop=Instant())
+
+ def as_utc(self) -> datetime:
+ """Instant as UTC datetime."""
+ return datetime.fromtimestamp(self.time, timezone.utc)
+
+
+@dataclasses.dataclass(frozen=True)
+class Duration:
+ """A span of time as measured by `Instant.elapsed()`."""
+
+ start: Instant
+ stop: Instant
+
+ @property
+ def seconds(self) -> float:
+ """Elapsed time of the duration in seconds, measured using a performance counter for precise timing."""
+ return self.stop.perf_count - self.start.perf_count
+
+
+@dataclasses.dataclass
+class MockTiming:
+ """Mocks _pytest.timing with a known object that can be used to control timing in tests
+ deterministically.
+
+ pytest itself should always use functions from `_pytest.timing` instead of `time` directly.
+
+ This then allows us more control over time during testing, if testing code also
+ uses `_pytest.timing` functions.
+
+ Time is static, and only advances through `sleep` calls, thus tests might sleep over large
+ numbers and obtain accurate time() calls at the end, making tests reliable and instant."""
+
+ _current_time: float = datetime(2020, 5, 22, 14, 20, 50).timestamp()
+
+ def sleep(self, seconds: float) -> None:
+ self._current_time += seconds
+
+ def time(self) -> float:
+ return self._current_time
+
+ def patch(self, monkeypatch: MonkeyPatch) -> None:
+ # pylint: disable-next=import-self
+ from _pytest import timing # noqa: PLW0406
+
+ monkeypatch.setattr(timing, "sleep", self.sleep)
+ monkeypatch.setattr(timing, "time", self.time)
+ monkeypatch.setattr(timing, "perf_counter", self.time)
+
+
+__all__ = ["perf_counter", "sleep", "time"]
diff --git a/venv/lib/python3.11/site-packages/_pytest/tmpdir.py b/venv/lib/python3.11/site-packages/_pytest/tmpdir.py
new file mode 100644
index 000000000..9196006e5
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/tmpdir.py
@@ -0,0 +1,351 @@
+# mypy: allow-untyped-defs
+"""Support for providing temporary directories to test functions."""
+
+from __future__ import annotations
+
+import atexit
+from collections.abc import Generator
+from contextlib import ExitStack
+import dataclasses
+import os
+from pathlib import Path
+import re
+from shutil import rmtree
+import stat
+import tempfile
+from typing import Any
+from typing import final
+from typing import Literal
+
+from .pathlib import cleanup_dead_symlinks
+from .pathlib import LOCK_TIMEOUT
+from .pathlib import make_numbered_dir
+from .pathlib import make_numbered_dir_with_cleanup
+from .pathlib import rm_rf
+from _pytest.compat import get_user_id
+from _pytest.config import Config
+from _pytest.config import ExitCode
+from _pytest.config import hookimpl
+from _pytest.config.argparsing import Parser
+from _pytest.deprecated import check_ispytest
+from _pytest.fixtures import fixture
+from _pytest.fixtures import FixtureRequest
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Item
+from _pytest.reports import TestReport
+from _pytest.stash import StashKey
+
+
+tmppath_result_key = StashKey[dict[str, bool]]()
+RetentionType = Literal["all", "failed", "none"]
+
+
+@final
+@dataclasses.dataclass
+class TempPathFactory:
+ """Factory for temporary directories under the common base temp directory,
+ as discussed at :ref:`temporary directory location and retention`.
+ """
+
+ _given_basetemp: Path | None
+ # pluggy TagTracerSub, not currently exposed, so Any.
+ _trace: Any
+ _basetemp: Path | None
+ _retention_count: int
+ _retention_policy: RetentionType
+
+ def __init__(
+ self,
+ given_basetemp: Path | None,
+ retention_count: int,
+ retention_policy: RetentionType,
+ trace,
+ basetemp: Path | None = None,
+ *,
+ _ispytest: bool = False,
+ ) -> None:
+ check_ispytest(_ispytest)
+ if given_basetemp is None:
+ self._given_basetemp = None
+ else:
+ # Use os.path.abspath() to get absolute path instead of resolve() as it
+ # does not work the same in all platforms (see #4427).
+ # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
+ self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
+ self._trace = trace
+ self._retention_count = retention_count
+ self._retention_policy = retention_policy
+ self._basetemp = basetemp
+ # Register cleanups for session finish. Also called atexit as a last
+ # resort if sessionfinish for some reason doesn't happen.
+ self._exit_stack = ExitStack()
+
+ @classmethod
+ def from_config(
+ cls,
+ config: Config,
+ *,
+ _ispytest: bool = False,
+ ) -> TempPathFactory:
+ """Create a factory according to pytest configuration.
+
+ :meta private:
+ """
+ check_ispytest(_ispytest)
+ count = int(config.getini("tmp_path_retention_count"))
+ if count < 0:
+ raise ValueError(
+ f"tmp_path_retention_count must be >= 0. Current input: {count}."
+ )
+
+ policy = config.getini("tmp_path_retention_policy")
+ if policy not in ("all", "failed", "none"):
+ raise ValueError(
+ f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
+ )
+
+ return cls(
+ given_basetemp=config.option.basetemp,
+ trace=config.trace.get("tmpdir"),
+ retention_count=count,
+ retention_policy=policy,
+ _ispytest=True,
+ )
+
+ def _ensure_relative_to_basetemp(self, basename: str) -> str:
+ basename = os.path.normpath(basename)
+ if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
+ raise ValueError(f"{basename} is not a normalized and relative path")
+ return basename
+
+ def mktemp(self, basename: str, numbered: bool = True) -> Path:
+ """Create a new temporary directory managed by the factory.
+
+ :param basename:
+ Directory base name, must be a relative path.
+
+ :param numbered:
+ If ``True``, ensure the directory is unique by adding a numbered
+ suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
+ means that this function will create directories named ``"foo-0"``,
+ ``"foo-1"``, ``"foo-2"`` and so on.
+
+ :returns:
+ The path to the new directory.
+ """
+ basename = self._ensure_relative_to_basetemp(basename)
+ if not numbered:
+ p = self.getbasetemp().joinpath(basename)
+ p.mkdir(mode=0o700)
+ else:
+ p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
+ self._trace("mktemp", p)
+ return p
+
+ def getbasetemp(self) -> Path:
+ """Return the base temporary directory, creating it if needed.
+
+ :returns:
+ The base temporary directory.
+ """
+ if self._basetemp is not None:
+ return self._basetemp
+
+ if self._given_basetemp is not None:
+ basetemp = self._given_basetemp
+ if basetemp.exists():
+ rm_rf(basetemp)
+ basetemp.mkdir(mode=0o700)
+ basetemp = basetemp.resolve()
+ else:
+ from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
+ temproot = Path(from_env or tempfile.gettempdir()).resolve()
+ user = get_user() or "unknown"
+ # use a sub-directory in the temproot to speed-up
+ # make_numbered_dir() call
+ rootdir = temproot.joinpath(f"pytest-of-{user}")
+ try:
+ rootdir.mkdir(mode=0o700, exist_ok=True)
+ except OSError:
+ # getuser() likely returned illegal characters for the platform, use unknown back off mechanism
+ rootdir = temproot.joinpath("pytest-of-unknown")
+ rootdir.mkdir(mode=0o700, exist_ok=True)
+ # Because we use exist_ok=True with a predictable name, make sure
+ # we are the owners, to prevent any funny business (on unix, where
+ # temproot is usually shared).
+ # Also, to keep things private, fixup any world-readable temp
+ # rootdir's permissions. Historically 0o755 was used, so we can't
+ # just error out on this, at least for a while.
+ # Don't follow symlinks, otherwise we're open to symlink-swapping
+ # TOCTOU vulnerability.
+ # This check makes us vulnerable to a DoS - a user can `mkdir
+ # /tmp/pytest-of-otheruser` and then `otheruser` will fail this
+ # check. For now we don't consider it a real problem. otheruser can
+ # change their TMPDIR or --basetemp, and maybe give the prankster a
+ # good scolding.
+ uid = get_user_id()
+ if uid is not None:
+ stat_follow_symlinks = (
+ False if os.stat in os.supports_follow_symlinks else True
+ )
+ rootdir_stat = rootdir.stat(follow_symlinks=stat_follow_symlinks)
+ if stat.S_ISLNK(rootdir_stat.st_mode):
+ raise OSError(
+ f"The temporary directory {rootdir} is a symbolic link. "
+ "Fix this and try again."
+ )
+ if rootdir_stat.st_uid != uid:
+ raise OSError(
+ f"The temporary directory {rootdir} is not owned by the current user. "
+ "Fix this and try again."
+ )
+ if (rootdir_stat.st_mode & 0o077) != 0:
+ chmod_follow_symlinks = (
+ False if os.chmod in os.supports_follow_symlinks else True
+ )
+ rootdir.chmod(
+ rootdir_stat.st_mode & ~0o077,
+ follow_symlinks=chmod_follow_symlinks,
+ )
+ keep = self._retention_count
+ if self._retention_policy == "none":
+ keep = 0
+ basetemp = make_numbered_dir_with_cleanup(
+ prefix="pytest-",
+ root=rootdir,
+ keep=keep,
+ lock_timeout=LOCK_TIMEOUT,
+ mode=0o700,
+ register=self._exit_stack.callback,
+ )
+ # Ensure that the cleanup is called on exit (#1120 possibly?).
+ # But if the exit stack is closed manually (as it normally should),
+ # unregister the atexit to avoid pile up.
+ atexit.register(self._exit_stack.close)
+ self._exit_stack.callback(atexit.unregister, self._exit_stack.close)
+ assert basetemp is not None, basetemp
+ self._basetemp = basetemp
+ self._trace("new basetemp", basetemp)
+ return basetemp
+
+
+def get_user() -> str | None:
+ """Return the current user name, or None if getuser() does not work
+ in the current environment (see #1010)."""
+ try:
+ # In some exotic environments, getpass may not be importable.
+ import getpass
+
+ return getpass.getuser()
+ except (ImportError, OSError, KeyError):
+ return None
+
+
+def pytest_configure(config: Config) -> None:
+ """Create a TempPathFactory and attach it to the config object.
+
+ This is to comply with existing plugins which expect the handler to be
+ available at pytest_configure time, but ideally should be moved entirely
+ to the tmp_path_factory session fixture.
+ """
+ mp = MonkeyPatch()
+ config.add_cleanup(mp.undo)
+ _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
+ mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
+
+
+def pytest_addoption(parser: Parser) -> None:
+ parser.addini(
+ "tmp_path_retention_count",
+ help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.",
+ default="3",
+ # NOTE: Would have been better as an `int` but can't change it now.
+ type="string",
+ )
+
+ parser.addini(
+ "tmp_path_retention_policy",
+ help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
+ "(all/failed/none)",
+ type="string",
+ default="all",
+ )
+
+
+@fixture(scope="session")
+def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
+ """Return a :class:`pytest.TempPathFactory` instance for the test session."""
+ # Set dynamically by pytest_configure() above.
+ return request.config._tmp_path_factory # type: ignore
+
+
+def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
+ name = request.node.name
+ name = re.sub(r"[\W]", "_", name)
+ MAXVAL = 30
+ name = name[:MAXVAL]
+ return factory.mktemp(name, numbered=True)
+
+
+@fixture
+def tmp_path(
+ request: FixtureRequest, tmp_path_factory: TempPathFactory
+) -> Generator[Path]:
+ """Return a temporary directory (as :class:`pathlib.Path` object)
+ which is unique to each test function invocation.
+ The temporary directory is created as a subdirectory
+ of the base temporary directory, with configurable retention,
+ as discussed in :ref:`temporary directory location and retention`.
+ """
+ path = _mk_tmp(request, tmp_path_factory)
+ yield path
+
+ # Remove the tmpdir if the policy is "failed" and the test passed.
+ policy = tmp_path_factory._retention_policy
+ result_dict = request.node.stash[tmppath_result_key]
+
+ if policy == "failed" and result_dict.get("call", True):
+ # We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
+ # permissions, etc, in which case we ignore it.
+ rmtree(path, ignore_errors=True)
+
+ del request.node.stash[tmppath_result_key]
+
+
+def pytest_sessionfinish(session, exitstatus: int | ExitCode):
+ """After each session, remove base directory if all the tests passed,
+ the policy is "failed", and the basetemp is not specified by a user.
+ """
+ tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
+ basetemp = tmp_path_factory._basetemp
+ if basetemp is None:
+ return
+
+ policy = tmp_path_factory._retention_policy
+ if (
+ exitstatus == 0
+ and policy == "failed"
+ and tmp_path_factory._given_basetemp is None
+ ):
+ if basetemp.is_dir():
+ # We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
+ # permissions, etc, in which case we ignore it.
+ rmtree(basetemp, ignore_errors=True)
+
+ # Remove dead symlinks.
+ if basetemp.is_dir():
+ cleanup_dead_symlinks(basetemp)
+
+ # Run the numbered dirs and lock file cleanups registered on the ExitStack.
+ tmp_path_factory._exit_stack.close()
+
+
+@hookimpl(wrapper=True, tryfirst=True)
+def pytest_runtest_makereport(
+ item: Item, call
+) -> Generator[None, TestReport, TestReport]:
+ rep = yield
+ assert rep.when is not None
+ empty: dict[str, bool] = {}
+ item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed
+ return rep
diff --git a/venv/lib/python3.11/site-packages/_pytest/tracemalloc.py b/venv/lib/python3.11/site-packages/_pytest/tracemalloc.py
new file mode 100644
index 000000000..5d0b19855
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/tracemalloc.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+
+def tracemalloc_message(source: object) -> str:
+ if source is None:
+ return ""
+
+ try:
+ import tracemalloc
+ except ImportError:
+ return ""
+
+ tb = tracemalloc.get_object_traceback(source)
+ if tb is not None:
+ formatted_tb = "\n".join(tb.format())
+ # Use a leading new line to better separate the (large) output
+ # from the traceback to the previous warning text.
+ return f"\nObject allocated at:\n{formatted_tb}"
+ # No need for a leading new line.
+ url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
+ return (
+ "Enable tracemalloc to get traceback where the object was allocated.\n"
+ f"See {url} for more info."
+ )
diff --git a/venv/lib/python3.11/site-packages/_pytest/unittest.py b/venv/lib/python3.11/site-packages/_pytest/unittest.py
new file mode 100644
index 000000000..d5286af14
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/unittest.py
@@ -0,0 +1,653 @@
+# mypy: allow-untyped-defs
+"""Discover and run std-library "unittest" style tests."""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from collections.abc import Generator
+from collections.abc import Iterable
+from collections.abc import Iterator
+from enum import auto
+from enum import Enum
+import inspect
+import sys
+import traceback
+import types
+from typing import Any
+from typing import TYPE_CHECKING
+from unittest import TestCase
+
+from _pytest import fixtures
+import _pytest._code
+from _pytest._code import ExceptionInfo
+from _pytest.compat import assert_never
+from _pytest.compat import is_async_function
+from _pytest.config import hookimpl
+from _pytest.fixtures import FixtureRequest
+from _pytest.monkeypatch import MonkeyPatch
+from _pytest.nodes import Collector
+from _pytest.nodes import Item
+from _pytest.outcomes import exit
+from _pytest.outcomes import fail
+from _pytest.outcomes import skip
+from _pytest.outcomes import xfail
+from _pytest.python import Class
+from _pytest.python import Function
+from _pytest.python import Module
+from _pytest.runner import CallInfo
+from _pytest.runner import check_interactive_exception
+from _pytest.subtests import SubtestContext
+from _pytest.subtests import SubtestReport
+
+
+if sys.version_info[:2] < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
+if TYPE_CHECKING:
+ from types import TracebackType
+ import unittest
+
+ import twisted.trial.unittest
+
+
+_SysExcInfoType = (
+ tuple[type[BaseException], BaseException, types.TracebackType]
+ | tuple[None, None, None]
+)
+
+
+def pytest_pycollect_makeitem(
+ collector: Module | Class, name: str, obj: object
+) -> UnitTestCase | None:
+ try:
+ # Has unittest been imported?
+ ut = sys.modules["unittest"]
+ # Is obj a subclass of unittest.TestCase?
+ # Type ignored because `ut` is an opaque module.
+ if not issubclass(obj, ut.TestCase): # type: ignore
+ return None
+ except Exception:
+ return None
+ # Is obj a concrete class?
+ # Abstract classes can't be instantiated so no point collecting them.
+ if inspect.isabstract(obj):
+ return None
+ # Yes, so let's collect it.
+ return UnitTestCase.from_parent(collector, name=name, obj=obj)
+
+
+class UnitTestCase(Class):
+ # Marker for fixturemanger.getfixtureinfo()
+ # to declare that our children do not support funcargs.
+ nofuncargs = True
+
+ def newinstance(self):
+ # TestCase __init__ takes the method (test) name. The TestCase
+ # constructor treats the name "runTest" as a special no-op, so it can be
+ # used when a dummy instance is needed. While unittest.TestCase has a
+ # default, some subclasses omit the default (#9610), so always supply
+ # it.
+ return self.obj("runTest")
+
+ def collect(self) -> Iterable[Item | Collector]:
+ from unittest import TestLoader
+
+ cls = self.obj
+ if not getattr(cls, "__test__", True):
+ return
+
+ skipped = _is_skipped(cls)
+ if not skipped:
+ self._register_unittest_setup_method_fixture(cls)
+ self._register_unittest_setup_class_fixture(cls)
+ self._register_setup_class_fixture()
+ else:
+ self._register_unittest_skip_fixture(cls)
+
+ self.session._fixturemanager.parsefactories(
+ holder=self.newinstance(), node=self
+ )
+
+ loader = TestLoader()
+ foundsomething = False
+ for name in loader.getTestCaseNames(self.obj):
+ x = getattr(self.obj, name)
+ if not getattr(x, "__test__", True):
+ continue
+ yield TestCaseFunction.from_parent(self, name=name)
+ foundsomething = True
+
+ if not foundsomething:
+ runtest = getattr(self.obj, "runTest", None)
+ if runtest is not None:
+ ut = sys.modules.get("twisted.trial.unittest", None)
+ if ut is None or runtest != ut.TestCase.runTest:
+ yield TestCaseFunction.from_parent(self, name="runTest")
+
+ def _register_unittest_setup_class_fixture(self, cls: type) -> None:
+ """Register an auto-use fixture to invoke setUpClass and
+ tearDownClass (#517)."""
+ setup = getattr(cls, "setUpClass", None)
+ teardown = getattr(cls, "tearDownClass", None)
+ if setup is None and teardown is None:
+ return None
+ cleanup = getattr(cls, "doClassCleanups", lambda: None)
+
+ def process_teardown_exceptions() -> None:
+ # tearDown_exceptions is a list set in the class containing exc_infos for errors during
+ # teardown for the class.
+ exc_infos = getattr(cls, "tearDown_exceptions", None)
+ if not exc_infos:
+ return
+ exceptions = [exc for (_, exc, _) in exc_infos]
+ # If a single exception, raise it directly as this provides a more readable
+ # error (hopefully this will improve in #12255).
+ if len(exceptions) == 1:
+ raise exceptions[0]
+ else:
+ raise ExceptionGroup("Unittest class cleanup errors", exceptions)
+
+ def unittest_setup_class_fixture(
+ request: FixtureRequest,
+ ) -> Generator[None]:
+ cls = request.cls
+ if _is_skipped(cls):
+ reason = cls.__unittest_skip_why__
+ raise skip.Exception(reason, _use_item_location=True)
+ if setup is not None:
+ try:
+ setup()
+ # unittest does not call the cleanup function for every BaseException, so we
+ # follow this here.
+ except Exception:
+ cleanup()
+ process_teardown_exceptions()
+ raise
+ yield
+ try:
+ if teardown is not None:
+ teardown()
+ finally:
+ cleanup()
+ process_teardown_exceptions()
+
+ fixtures.register_fixture(
+ # Use a unique name to speed up lookup.
+ name=f"_unittest_setUpClass_fixture_{cls.__qualname__}",
+ func=unittest_setup_class_fixture,
+ node=self,
+ scope="class",
+ autouse=True,
+ )
+
+ def _register_unittest_skip_fixture(self, cls: type) -> None:
+ """Register an auto-use fixture to skip tests for a class decorated
+ with @unittest.skip or @unittest.skipIf (#13885)."""
+
+ def unittest_skip_fixture(request: FixtureRequest) -> None:
+ reason = getattr(cls, "__unittest_skip_why__", "")
+ raise skip.Exception(reason, _use_item_location=True)
+
+ fixtures.register_fixture(
+ name=f"_unittest_skip_fixture_{cls.__qualname__}",
+ func=unittest_skip_fixture,
+ node=self,
+ scope="class",
+ autouse=True,
+ )
+
+ def _register_unittest_setup_method_fixture(self, cls: type) -> None:
+ """Register an auto-use fixture to invoke setup_method and
+ teardown_method (#517)."""
+ setup = getattr(cls, "setup_method", None)
+ teardown = getattr(cls, "teardown_method", None)
+ if setup is None and teardown is None:
+ return None
+
+ def unittest_setup_method_fixture(
+ request: FixtureRequest,
+ ) -> Generator[None]:
+ self = request.instance
+ if _is_skipped(self):
+ reason = self.__unittest_skip_why__
+ raise skip.Exception(reason, _use_item_location=True)
+ if setup is not None:
+ setup(self, request.function)
+ yield
+ if teardown is not None:
+ teardown(self, request.function)
+
+ fixtures.register_fixture(
+ # Use a unique name to speed up lookup.
+ name=f"_unittest_setup_method_fixture_{cls.__qualname__}",
+ func=unittest_setup_method_fixture,
+ node=self,
+ scope="function",
+ autouse=True,
+ )
+
+
+class TestCaseFunction(Function):
+ nofuncargs = True
+ failfast = False
+ _excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None
+
+ def _getinstance(self):
+ assert isinstance(self.parent, UnitTestCase)
+ return self.parent.obj(self.name)
+
+ # Backward compat for pytest-django; can be removed after pytest-django
+ # updates + some slack.
+ @property
+ def _testcase(self):
+ return self.instance
+
+ def setup(self) -> None:
+ # A bound method to be called during teardown() if set (see 'runtest()').
+ self._explicit_tearDown: Callable[[], None] | None = None
+ super().setup()
+ if sys.version_info < (3, 11):
+ # A cache of the subTest errors and non-subtest skips in self._outcome.
+ # Compute and cache these lists once, instead of computing them again and again for each subtest (#13965).
+ self._cached_errors_and_skips: tuple[list[Any], list[Any]] | None = None
+
+ def teardown(self) -> None:
+ if self._explicit_tearDown is not None:
+ self._explicit_tearDown()
+ self._explicit_tearDown = None
+ self._obj = None
+ del self._instance
+ super().teardown()
+
+ def startTest(self, testcase: unittest.TestCase) -> None:
+ pass
+
+ def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None:
+ rawexcinfo = _handle_twisted_exc_info(rawexcinfo)
+ try:
+ excinfo = _pytest._code.ExceptionInfo[BaseException].from_exc_info(
+ rawexcinfo # type: ignore[arg-type]
+ )
+ # Invoke the attributes to trigger storing the traceback
+ # trial causes some issue there.
+ _ = excinfo.value
+ _ = excinfo.traceback
+ except TypeError:
+ try:
+ try:
+ values = traceback.format_exception(*rawexcinfo)
+ values.insert(
+ 0,
+ "NOTE: Incompatible Exception Representation, "
+ "displaying natively:\n\n",
+ )
+ fail("".join(values), pytrace=False)
+ except (fail.Exception, KeyboardInterrupt):
+ raise
+ except BaseException:
+ fail(
+ "ERROR: Unknown Incompatible Exception "
+ f"representation:\n{rawexcinfo!r}",
+ pytrace=False,
+ )
+ except KeyboardInterrupt:
+ raise
+ except fail.Exception:
+ excinfo = _pytest._code.ExceptionInfo.from_current()
+ self.__dict__.setdefault("_excinfo", []).append(excinfo)
+
+ def addError(
+ self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType
+ ) -> None:
+ try:
+ if isinstance(rawexcinfo[1], exit.Exception):
+ exit(rawexcinfo[1].msg)
+ except TypeError:
+ pass
+ self._addexcinfo(rawexcinfo)
+
+ def addFailure(
+ self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType
+ ) -> None:
+ self._addexcinfo(rawexcinfo)
+
+ def addSkip(
+ self, testcase: unittest.TestCase, reason: str, *, handle_subtests: bool = True
+ ) -> None:
+ from unittest.case import _SubTest # type: ignore[attr-defined]
+
+ def add_skip() -> None:
+ try:
+ raise skip.Exception(reason, _use_item_location=True)
+ except skip.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ if not handle_subtests:
+ add_skip()
+ return
+
+ if isinstance(testcase, _SubTest):
+ add_skip()
+ if self._excinfo is not None:
+ exc_info = self._excinfo[-1]
+ self.addSubTest(testcase.test_case, testcase, exc_info)
+ else:
+ # For python < 3.11: the non-subtest skips have to be added by `add_skip` only after all subtest
+ # failures are processed by `_addSubTest`: `self.instance._outcome` has no attribute
+ # `skipped/errors` anymore.
+ # We also need to check if `self.instance._outcome` is `None` (this happens if the test
+ # class/method is decorated with `unittest.skip`, see pytest-dev/pytest-subtests#173).
+ if sys.version_info < (3, 11) and self.instance._outcome is not None:
+ subtest_errors, _ = self._obtain_errors_and_skips()
+ if len(subtest_errors) == 0:
+ add_skip()
+ else:
+ add_skip()
+
+ def addExpectedFailure(
+ self,
+ testcase: unittest.TestCase,
+ rawexcinfo: _SysExcInfoType,
+ reason: str = "",
+ ) -> None:
+ try:
+ xfail(str(reason))
+ except xfail.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addUnexpectedSuccess(
+ self,
+ testcase: unittest.TestCase,
+ reason: twisted.trial.unittest.Todo | None = None,
+ ) -> None:
+ msg = "Unexpected success"
+ if reason:
+ msg += f": {reason.reason}"
+ # Preserve unittest behaviour - fail the test. Explicitly not an XPASS.
+ try:
+ fail(msg, pytrace=False)
+ except fail.Exception:
+ self._addexcinfo(sys.exc_info())
+
+ def addSuccess(self, testcase: unittest.TestCase) -> None:
+ pass
+
+ def stopTest(self, testcase: unittest.TestCase) -> None:
+ pass
+
+ def addDuration(self, testcase: unittest.TestCase, elapsed: float) -> None:
+ pass
+
+ def runtest(self) -> None:
+ from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
+
+ testcase = self.instance
+ assert testcase is not None
+
+ maybe_wrap_pytest_function_for_tracing(self)
+
+ # Let the unittest framework handle async functions.
+ if is_async_function(self.obj):
+ testcase(result=self)
+ else:
+ # When --pdb is given, we want to postpone calling tearDown() otherwise
+ # when entering the pdb prompt, tearDown() would have probably cleaned up
+ # instance variables, which makes it difficult to debug.
+ # Arguably we could always postpone tearDown(), but this changes the moment where the
+ # TestCase instance interacts with the results object, so better to only do it
+ # when absolutely needed.
+ # We need to consider if the test itself is skipped, or the whole class.
+ assert isinstance(self.parent, UnitTestCase)
+ skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj)
+ if self.config.getoption("usepdb") and not skipped:
+ self._explicit_tearDown = testcase.tearDown
+ setattr(testcase, "tearDown", lambda *args: None)
+
+ # We need to update the actual bound method with self.obj, because
+ # wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
+ setattr(testcase, self.name, self.obj)
+ try:
+ testcase(result=self)
+ finally:
+ delattr(testcase, self.name)
+
+ def _traceback_filter(
+ self, excinfo: _pytest._code.ExceptionInfo[BaseException]
+ ) -> _pytest._code.Traceback:
+ traceback = super()._traceback_filter(excinfo)
+ ntraceback = traceback.filter(
+ lambda x: not x.frame.f_globals.get("__unittest"),
+ )
+ if not ntraceback:
+ ntraceback = traceback
+ return ntraceback
+
+ def addSubTest(
+ self,
+ test_case: Any,
+ test: TestCase,
+ exc_info: ExceptionInfo[BaseException]
+ | tuple[type[BaseException], BaseException, TracebackType]
+ | None,
+ ) -> None:
+ # Importing this private symbol locally in case this symbol is renamed/removed in the future; importing
+ # it globally would break pytest entirely, importing it locally only will break unittests using `addSubTest`.
+ from unittest.case import _subtest_msg_sentinel # type: ignore[attr-defined]
+
+ exception_info: ExceptionInfo[BaseException] | None
+ match exc_info:
+ case tuple():
+ exception_info = ExceptionInfo(exc_info, _ispytest=True)
+ case ExceptionInfo() | None:
+ exception_info = exc_info
+ case unreachable:
+ assert_never(unreachable)
+
+ call_info = CallInfo[None](
+ None,
+ exception_info,
+ start=0,
+ stop=0,
+ duration=0,
+ when="call",
+ _ispytest=True,
+ )
+ msg = None if test._message is _subtest_msg_sentinel else str(test._message) # type: ignore[attr-defined]
+ report = self.ihook.pytest_runtest_makereport(item=self, call=call_info)
+ sub_report = SubtestReport._new(
+ report,
+ SubtestContext(msg=msg, kwargs=dict(test.params)), # type: ignore[attr-defined]
+ captured_output=None,
+ captured_logs=None,
+ )
+ self.ihook.pytest_runtest_logreport(report=sub_report)
+ if check_interactive_exception(call_info, sub_report):
+ self.ihook.pytest_exception_interact(
+ node=self, call=call_info, report=sub_report
+ )
+
+ # For python < 3.11: add non-subtest skips once all subtest failures are processed by # `_addSubTest`.
+ if sys.version_info < (3, 11):
+ subtest_errors, non_subtest_skip = self._obtain_errors_and_skips()
+
+ # Check if we have non-subtest skips: if there are also sub failures, non-subtest skips are not treated in
+ # `_addSubTest` and have to be added using `add_skip` after all subtest failures are processed.
+ if len(non_subtest_skip) > 0 and len(subtest_errors) > 0:
+ # Make sure we have processed the last subtest failure
+ last_subset_error = subtest_errors[-1]
+ if exc_info is last_subset_error[-1]:
+ # Add non-subtest skips (as they could not be treated in `_addSkip`)
+ for testcase, reason in non_subtest_skip:
+ self.addSkip(testcase, reason, handle_subtests=False)
+
+ def _obtain_errors_and_skips(self) -> tuple[list[Any], list[Any]]:
+ """Compute or obtain the cached values for subtest errors and non-subtest skips."""
+ from unittest.case import _SubTest # type: ignore[attr-defined]
+
+ assert sys.version_info < (3, 11), (
+ "This workaround only should be used in Python 3.10"
+ )
+ if self._cached_errors_and_skips is not None:
+ return self._cached_errors_and_skips
+
+ subtest_errors = [
+ (x, y)
+ for x, y in self.instance._outcome.errors
+ if isinstance(x, _SubTest) and y is not None
+ ]
+
+ non_subtest_skips = [
+ (x, y)
+ for x, y in self.instance._outcome.skipped
+ if not isinstance(x, _SubTest)
+ ]
+ self._cached_errors_and_skips = (subtest_errors, non_subtest_skips)
+ return subtest_errors, non_subtest_skips
+
+
+@hookimpl(tryfirst=True)
+def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
+ if isinstance(item, TestCaseFunction):
+ if item._excinfo:
+ call.excinfo = item._excinfo.pop(0)
+ try:
+ del call.result
+ except AttributeError:
+ pass
+
+ # Convert unittest.SkipTest to pytest.skip.
+ # This covers explicit `raise unittest.SkipTest`.
+ unittest = sys.modules.get("unittest")
+ if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest):
+ excinfo = call.excinfo
+ call2 = CallInfo[None].from_call(lambda: skip(str(excinfo.value)), call.when)
+ call.excinfo = call2.excinfo
+
+
+def _is_skipped(obj) -> bool:
+ """Return True if the given object has been marked with @unittest.skip."""
+ return bool(getattr(obj, "__unittest_skip__", False))
+
+
+def pytest_configure() -> None:
+ """Register the TestCaseFunction class as an IReporter if twisted.trial is available."""
+ if _get_twisted_version() is not TwistedVersion.NotInstalled:
+ from twisted.trial.itrial import IReporter
+ from zope.interface import classImplements
+
+ classImplements(TestCaseFunction, IReporter)
+
+
+class TwistedVersion(Enum):
+ """
+ The Twisted version installed in the environment.
+
+ We have different workarounds in place for different versions of Twisted.
+ """
+
+ # Twisted version 24 or prior.
+ Version24 = auto()
+ # Twisted version 25 or later.
+ Version25 = auto()
+ # Twisted version is not available.
+ NotInstalled = auto()
+
+
+def _get_twisted_version() -> TwistedVersion:
+ # We need to check if "twisted.trial.unittest" is specifically present in sys.modules.
+ # This is because we intend to integrate with Trial only when it's actively running
+ # the test suite, but not needed when only other Twisted components are in use.
+ if "twisted.trial.unittest" not in sys.modules:
+ return TwistedVersion.NotInstalled
+
+ import importlib.metadata
+
+ import packaging.version
+
+ version_str = importlib.metadata.version("twisted")
+ version = packaging.version.parse(version_str)
+ if version.major <= 24:
+ return TwistedVersion.Version24
+ else:
+ return TwistedVersion.Version25
+
+
+# Name of the attribute in `twisted.python.Failure` instances that stores
+# the `sys.exc_info()` tuple.
+# See twisted.trial support in `pytest_runtest_protocol`.
+TWISTED_RAW_EXCINFO_ATTR = "_twisted_raw_excinfo"
+
+
+@hookimpl(wrapper=True)
+def pytest_runtest_protocol(item: Item) -> Iterator[None]:
+ if _get_twisted_version() is TwistedVersion.Version24:
+ import twisted.python.failure as ut
+
+ # Monkeypatch `Failure.__init__` to store the raw exception info.
+ original__init__ = ut.Failure.__init__
+
+ def store_raw_exception_info(
+ self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
+ ): # pragma: no cover
+ if exc_value is None:
+ raw_exc_info = sys.exc_info()
+ else:
+ if exc_type is None:
+ exc_type = type(exc_value)
+ if exc_tb is None:
+ exc_tb = sys.exc_info()[2]
+ raw_exc_info = (exc_type, exc_value, exc_tb)
+ setattr(self, TWISTED_RAW_EXCINFO_ATTR, tuple(raw_exc_info))
+ try:
+ original__init__(
+ self, exc_value, exc_type, exc_tb, captureVars=captureVars
+ )
+ except TypeError: # pragma: no cover
+ original__init__(self, exc_value, exc_type, exc_tb)
+
+ with MonkeyPatch.context() as patcher:
+ patcher.setattr(ut.Failure, "__init__", store_raw_exception_info)
+ return (yield)
+ else:
+ return (yield)
+
+
+def _handle_twisted_exc_info(
+ rawexcinfo: _SysExcInfoType | BaseException,
+) -> _SysExcInfoType:
+ """
+ Twisted passes a custom Failure instance to `addError()` instead of using `sys.exc_info()`.
+ Therefore, if `rawexcinfo` is a `Failure` instance, convert it into the equivalent `sys.exc_info()` tuple
+ as expected by pytest.
+ """
+ twisted_version = _get_twisted_version()
+ if twisted_version is TwistedVersion.NotInstalled:
+ # Unfortunately, because we cannot import `twisted.python.failure` at the top of the file
+ # and use it in the signature, we need to use `type:ignore` here because we cannot narrow
+ # the type properly in the `if` statement above.
+ return rawexcinfo # type:ignore[return-value]
+ elif twisted_version is TwistedVersion.Version24:
+ # Twisted calls addError() passing its own classes (like `twisted.python.Failure`), which violates
+ # the `addError()` signature, so we extract the original `sys.exc_info()` tuple which is stored
+ # in the object.
+ if hasattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR):
+ saved_exc_info = getattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR)
+ # Delete the attribute from the original object to avoid leaks.
+ delattr(rawexcinfo, TWISTED_RAW_EXCINFO_ATTR)
+ return saved_exc_info # type:ignore[no-any-return]
+ return rawexcinfo # type:ignore[return-value]
+ elif twisted_version is TwistedVersion.Version25:
+ if isinstance(rawexcinfo, BaseException):
+ import twisted.python.failure
+
+ if isinstance(rawexcinfo, twisted.python.failure.Failure):
+ tb = rawexcinfo.__traceback__
+ if tb is None:
+ tb = sys.exc_info()[2]
+ return type(rawexcinfo.value), rawexcinfo.value, tb
+
+ return rawexcinfo # type:ignore[return-value]
+ else:
+ # Ideally we would use assert_never() here, but it is not available in all Python versions
+ # we support, plus we do not require `type_extensions` currently.
+ assert False, f"Unexpected Twisted version: {twisted_version}"
diff --git a/venv/lib/python3.11/site-packages/_pytest/unraisableexception.py b/venv/lib/python3.11/site-packages/_pytest/unraisableexception.py
new file mode 100644
index 000000000..6c092fb6b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/unraisableexception.py
@@ -0,0 +1,188 @@
+from __future__ import annotations
+
+import collections
+from collections.abc import Callable
+import functools
+import gc
+import sys
+import traceback
+from typing import NamedTuple
+from typing import TYPE_CHECKING
+import warnings
+
+from _pytest.config import Config
+from _pytest.nodes import Item
+from _pytest.stash import StashKey
+from _pytest.tracemalloc import tracemalloc_message
+import pytest
+
+
+if TYPE_CHECKING:
+ pass
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
+
+# This is a stash item and not a simple constant to allow pytester to override it.
+gc_collect_iterations_key = StashKey[int]()
+
+
+def gc_collect_harder(iterations: int) -> None:
+ for _ in range(iterations):
+ gc.collect()
+
+
+class UnraisableMeta(NamedTuple):
+ msg: str
+ cause_msg: str
+ exc_value: BaseException | None
+
+
+unraisable_exceptions: StashKey[collections.deque[UnraisableMeta | BaseException]] = (
+ StashKey()
+)
+
+
+def collect_unraisable(config: Config) -> None:
+ pop_unraisable = config.stash[unraisable_exceptions].pop
+ errors: list[pytest.PytestUnraisableExceptionWarning | RuntimeError] = []
+ meta = None
+ hook_error = None
+ try:
+ while True:
+ try:
+ meta = pop_unraisable()
+ except IndexError:
+ break
+
+ if isinstance(meta, BaseException):
+ hook_error = RuntimeError("Failed to process unraisable exception")
+ hook_error.__cause__ = meta
+ errors.append(hook_error)
+ continue
+
+ msg = meta.msg
+ try:
+ warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
+ except pytest.PytestUnraisableExceptionWarning as e:
+ # This except happens when the warning is treated as an error (e.g. `-Werror`).
+ if meta.exc_value is not None:
+ # Exceptions have a better way to show the traceback, but
+ # warnings do not, so hide the traceback from the msg and
+ # set the cause so the traceback shows up in the right place.
+ e.args = (meta.cause_msg,)
+ e.__cause__ = meta.exc_value
+ errors.append(e)
+
+ if len(errors) == 1:
+ raise errors[0]
+ if errors:
+ raise ExceptionGroup("multiple unraisable exception warnings", errors)
+ finally:
+ del errors, meta, hook_error
+
+
+def cleanup(
+ *, config: Config, prev_hook: Callable[[sys.UnraisableHookArgs], object]
+) -> None:
+ # On PyPy, objects (e.g. coroutines) can survive GC rounds because executing
+ # their __del__ can resurrect them. The Trio project determined experimentally
+ # that 5 passes are needed on PyPy to flush everything. On CPython, reference
+ # counting handles most cleanup immediately, so 1 pass is sufficient.
+ _default_gc_collect_iterations = 5 if sys.implementation.name == "pypy" else 1
+ gc_collect_iterations = config.stash.get(
+ gc_collect_iterations_key, _default_gc_collect_iterations
+ )
+ try:
+ try:
+ gc_collect_harder(gc_collect_iterations)
+ collect_unraisable(config)
+ finally:
+ sys.unraisablehook = prev_hook
+ finally:
+ del config.stash[unraisable_exceptions]
+
+
+def unraisable_hook(
+ unraisable: sys.UnraisableHookArgs,
+ /,
+ *,
+ append: Callable[[UnraisableMeta | BaseException], object],
+) -> None:
+ try:
+ # we need to compute these strings here as they might change after
+ # the unraisablehook finishes and before the metadata object is
+ # collected by a pytest hook
+ err_msg = (
+ "Exception ignored in" if unraisable.err_msg is None else unraisable.err_msg
+ )
+ summary = f"{err_msg}: {unraisable.object!r}"
+ traceback_message = "\n\n" + "".join(
+ traceback.format_exception(
+ unraisable.exc_type,
+ unraisable.exc_value,
+ unraisable.exc_traceback,
+ )
+ )
+ tracemalloc_tb = "\n" + tracemalloc_message(unraisable.object)
+ msg = summary + traceback_message + tracemalloc_tb
+ cause_msg = summary + tracemalloc_tb
+
+ append(
+ UnraisableMeta(
+ msg=msg,
+ cause_msg=cause_msg,
+ exc_value=unraisable.exc_value,
+ )
+ )
+ except BaseException as e:
+ append(e)
+ # Raising this will cause the exception to be logged twice, once in our
+ # collect_unraisable and once by the unraisablehook calling machinery
+ # which is fine - this should never happen anyway and if it does
+ # it should probably be reported as a pytest bug.
+ raise
+
+
+def pytest_configure(config: Config) -> None:
+ prev_hook = sys.unraisablehook
+ deque: collections.deque[UnraisableMeta | BaseException] = collections.deque()
+ config.stash[unraisable_exceptions] = deque
+ config.add_cleanup(functools.partial(cleanup, config=config, prev_hook=prev_hook))
+ sys.unraisablehook = functools.partial(unraisable_hook, append=deque.append)
+
+
+def pytest_unconfigure(config: Config) -> None:
+ # Runs before ``_cleanup_stack.close()``, so warning filters from
+ # cleanup-stack-managed contexts (notably the ``warnings`` plugin's
+ # ``catch_warnings``) are still installed when garbage-collected
+ # finalizers fire. A ``config.add_cleanup`` callback would instead
+ # couple correctness to LIFO pop order across plugins' cleanups.
+ if unraisable_exceptions not in config.stash:
+ # ``pytest_configure`` did not complete (e.g. a usage error raised
+ # in another plugin's configure), so the queue stash was never set.
+ return
+ # PyPy can resurrect objects in __del__, so it needs several GC passes
+ # (5, per the Trio project); CPython frees cycles in one pass. See #14441.
+ _default_gc_collect_iterations = 5 if sys.implementation.name == "pypy" else 1
+ gc_collect_iterations = config.stash.get(
+ gc_collect_iterations_key, _default_gc_collect_iterations
+ )
+ gc_collect_harder(gc_collect_iterations)
+ collect_unraisable(config)
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_runtest_setup(item: Item) -> None:
+ collect_unraisable(item.config)
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_runtest_call(item: Item) -> None:
+ collect_unraisable(item.config)
+
+
+@pytest.hookimpl(trylast=True)
+def pytest_runtest_teardown(item: Item) -> None:
+ collect_unraisable(item.config)
diff --git a/venv/lib/python3.11/site-packages/_pytest/warning_types.py b/venv/lib/python3.11/site-packages/_pytest/warning_types.py
new file mode 100644
index 000000000..8fd62ff84
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/warning_types.py
@@ -0,0 +1,166 @@
+from __future__ import annotations
+
+import dataclasses
+import inspect
+from types import FunctionType
+from typing import Any
+from typing import final
+from typing import Generic
+from typing import TypeVar
+import warnings
+
+
+class PytestWarning(UserWarning):
+ """Base class for all warnings emitted by pytest."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestAssertRewriteWarning(PytestWarning):
+ """Warning emitted by the pytest assert rewrite module."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestCacheWarning(PytestWarning):
+ """Warning emitted by the cache plugin in various situations."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestConfigWarning(PytestWarning):
+ """Warning emitted for configuration issues."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestCollectionWarning(PytestWarning):
+ """Warning emitted when pytest is not able to collect a file or symbol in a module."""
+
+ __module__ = "pytest"
+
+
+class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
+ """Warning class for features that will be removed in a future version."""
+
+ __module__ = "pytest"
+
+
+class PytestRemovedIn10Warning(PytestDeprecationWarning):
+ """Warning class for features that will be removed in pytest 10."""
+
+ __module__ = "pytest"
+
+
+@final
+class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
+ """Warning category used to denote experiments in pytest.
+
+ Use sparingly as the API might change or even be removed completely in a
+ future version.
+ """
+
+ __module__ = "pytest"
+
+ @classmethod
+ def simple(cls, apiname: str) -> PytestExperimentalApiWarning:
+ return cls(f"{apiname} is an experimental api that may change over time")
+
+
+@final
+class PytestReturnNotNoneWarning(PytestWarning):
+ """
+ Warning emitted when a test function returns a value other than ``None``.
+
+ See :ref:`return-not-none` for details.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnknownMarkWarning(PytestWarning):
+ """Warning emitted on use of unknown markers.
+
+ See :ref:`mark` for details.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnraisableExceptionWarning(PytestWarning):
+ """An unraisable exception was reported.
+
+ Unraisable exceptions are exceptions raised in :meth:`__del__ `
+ implementations and similar situations when the exception cannot be raised
+ as normal.
+ """
+
+ __module__ = "pytest"
+
+
+@final
+class PytestUnhandledThreadExceptionWarning(PytestWarning):
+ """An unhandled exception occurred in a :class:`~threading.Thread`.
+
+ Such exceptions don't propagate normally.
+ """
+
+ __module__ = "pytest"
+
+
+_W = TypeVar("_W", bound=PytestWarning)
+
+
+@final
+@dataclasses.dataclass
+class UnformattedWarning(Generic[_W]):
+ """A warning meant to be formatted during runtime.
+
+ This is used to hold warnings that need to format their message at runtime,
+ as opposed to a direct message.
+ """
+
+ category: type[_W]
+ template: str
+
+ def format(self, **kwargs: Any) -> _W:
+ """Return an instance of the warning category, formatted with given kwargs."""
+ return self.category(self.template.format(**kwargs))
+
+
+@final
+class PytestFDWarning(PytestWarning):
+ """When the lsof plugin finds leaked fds."""
+
+ __module__ = "pytest"
+
+
+def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
+ """
+ Issue the warning :param:`message` for the definition of the given :param:`method`
+
+ this helps to log warnings for functions defined prior to finding an issue with them
+ (like hook wrappers being marked in a legacy mechanism)
+ """
+ lineno = method.__code__.co_firstlineno
+ filename = inspect.getfile(method)
+ module = method.__module__
+ mod_globals = method.__globals__
+ try:
+ warnings.warn_explicit(
+ message,
+ type(message),
+ filename=filename,
+ module=module,
+ registry=mod_globals.setdefault("__warningregistry__", {}),
+ lineno=lineno,
+ )
+ except Warning as w:
+ # If warnings are errors (e.g. -Werror), location information gets lost, so we add it to the message.
+ raise type(w)(f"{w}\n at {filename}:{lineno}") from None
diff --git a/venv/lib/python3.11/site-packages/_pytest/warnings.py b/venv/lib/python3.11/site-packages/_pytest/warnings.py
new file mode 100644
index 000000000..d599d6c27
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/_pytest/warnings.py
@@ -0,0 +1,152 @@
+# mypy: allow-untyped-defs
+from __future__ import annotations
+
+from collections.abc import Generator
+from contextlib import contextmanager
+from contextlib import ExitStack
+import sys
+from typing import Literal
+import warnings
+
+from _pytest.config import apply_warning_filters
+from _pytest.config import Config
+from _pytest.config import parse_warning_filter
+from _pytest.main import Session
+from _pytest.nodes import Item
+from _pytest.terminal import TerminalReporter
+from _pytest.tracemalloc import tracemalloc_message
+import pytest
+
+
+@contextmanager
+def catch_warnings_for_item(
+ config: Config,
+ ihook,
+ when: Literal["config", "collect", "runtest"],
+ item: Item | None,
+ *,
+ record: bool = True,
+) -> Generator[None]:
+ """Context manager that catches warnings generated in the contained execution block.
+
+ ``item`` can be None if we are not in the context of an item execution.
+
+ Each warning captured triggers the ``pytest_warning_recorded`` hook.
+ """
+ config_filters = config.getini("filterwarnings")
+ cmdline_filters = config.known_args_namespace.pythonwarnings or []
+ with warnings.catch_warnings(record=record) as log:
+ if not sys.warnoptions:
+ # If user is not explicitly configuring warning filters, show deprecation warnings by default (#2908).
+ warnings.filterwarnings("always", category=DeprecationWarning)
+ warnings.filterwarnings("always", category=PendingDeprecationWarning)
+
+ # To be enabled in pytest 10.0.0.
+ # warnings.filterwarnings("error", category=pytest.PytestRemovedIn10Warning)
+
+ apply_warning_filters(config_filters, cmdline_filters)
+
+ # apply filters from "filterwarnings" marks
+ nodeid = "" if item is None else item.nodeid
+ if item is not None:
+ for mark in item.iter_markers(name="filterwarnings"):
+ for arg in mark.args:
+ warnings.filterwarnings(*parse_warning_filter(arg, escape=False))
+
+ try:
+ yield
+ finally:
+ if record:
+ # mypy can't infer that record=True means log is not None; help it.
+ assert log is not None
+
+ for warning_message in log:
+ ihook.pytest_warning_recorded.call_historic(
+ kwargs=dict(
+ warning_message=warning_message,
+ nodeid=nodeid,
+ when=when,
+ location=None,
+ )
+ )
+
+
+def warning_record_to_str(warning_message: warnings.WarningMessage) -> str:
+ """Convert a warnings.WarningMessage to a string."""
+ return warnings.formatwarning(
+ str(warning_message.message),
+ warning_message.category,
+ warning_message.filename,
+ warning_message.lineno,
+ warning_message.line,
+ ) + tracemalloc_message(warning_message.source)
+
+
+@pytest.hookimpl(wrapper=True, tryfirst=True)
+def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
+ with catch_warnings_for_item(
+ config=item.config, ihook=item.ihook, when="runtest", item=item
+ ):
+ return (yield)
+
+
+@pytest.hookimpl(wrapper=True, tryfirst=True)
+def pytest_collection(session: Session) -> Generator[None, object, object]:
+ config = session.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="collect", item=None
+ ):
+ return (yield)
+
+
+@pytest.hookimpl(wrapper=True)
+def pytest_terminal_summary(
+ terminalreporter: TerminalReporter,
+) -> Generator[None]:
+ config = terminalreporter.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="config", item=None
+ ):
+ return (yield)
+
+
+@pytest.hookimpl(wrapper=True)
+def pytest_sessionfinish(session: Session) -> Generator[None]:
+ config = session.config
+ with catch_warnings_for_item(
+ config=config, ihook=config.hook, when="config", item=None
+ ):
+ return (yield)
+
+
+@pytest.hookimpl(wrapper=True)
+def pytest_load_initial_conftests(
+ early_config: Config,
+) -> Generator[None]:
+ with catch_warnings_for_item(
+ config=early_config, ihook=early_config.hook, when="config", item=None
+ ):
+ return (yield)
+
+
+def pytest_configure(config: Config) -> None:
+ with ExitStack() as stack:
+ stack.enter_context(
+ catch_warnings_for_item(
+ config=config,
+ ihook=config.hook,
+ when="config",
+ item=None,
+ # this disables recording because the terminalreporter has
+ # finished by the time it comes to reporting logged warnings
+ # from the end of config cleanup. So for now, this is only
+ # useful for setting a warning filter with an 'error' action.
+ record=False,
+ )
+ )
+ config.addinivalue_line(
+ "markers",
+ "filterwarnings(warning): add a warning filter to the given test. "
+ "see https://docs.pytest.org/en/stable/how-to/capture-warnings.html#pytest-mark-filterwarnings ",
+ )
+ config.add_cleanup(stack.pop_all().close)
diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/INSTALLER
new file mode 100644
index 000000000..a1b589e38
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/METADATA b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/METADATA
new file mode 100644
index 000000000..d162dc81d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/METADATA
@@ -0,0 +1,144 @@
+Metadata-Version: 2.4
+Name: annotated-doc
+Version: 0.0.5
+Summary: Document parameters, class attributes, return types, and variables inline, with Annotated.
+Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?=
+License-Expression: MIT
+License-File: LICENSE
+Classifier: Intended Audience :: Information Technology
+Classifier: Intended Audience :: System Administrators
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python
+Classifier: Topic :: Internet
+Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Topic :: Software Development
+Classifier: Typing :: Typed
+Classifier: Development Status :: 4 - Beta
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Project-URL: Homepage, https://github.com/fastapi/annotated-doc
+Project-URL: Documentation, https://github.com/fastapi/annotated-doc
+Project-URL: Repository, https://github.com/fastapi/annotated-doc
+Project-URL: Issues, https://github.com/fastapi/annotated-doc/issues
+Project-URL: Changelog, https://github.com/fastapi/annotated-doc/release-notes.md
+Requires-Python: >=3.9
+Description-Content-Type: text/markdown
+
+# Annotated Doc
+
+Document parameters, class attributes, return types, and variables inline, with `Annotated`.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Installation
+
+```bash
+pip install annotated-doc
+```
+
+Or with `uv`:
+
+```Python
+uv add annotated-doc
+```
+
+## Usage
+
+Import `Doc` and pass a single literal string with the documentation for the specific parameter, class attribute, return type, or variable.
+
+For example, to document a parameter `name` in a function `hi` you could do:
+
+```Python
+from typing import Annotated
+
+from annotated_doc import Doc
+
+def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
+ print(f"Hi, {name}!")
+```
+
+You can also use it to document class attributes:
+
+```Python
+from typing import Annotated
+
+from annotated_doc import Doc
+
+class User:
+ name: Annotated[str, Doc("The user's name")]
+ age: Annotated[int, Doc("The user's age")]
+```
+
+The same way, you could document return types and variables, or anything that could have a type annotation with `Annotated`.
+
+## Who Uses This
+
+`annotated-doc` was made for:
+
+* [FastAPI](https://fastapi.tiangolo.com/)
+* [Typer](https://typer.tiangolo.com/)
+* [SQLModel](https://sqlmodel.tiangolo.com/)
+* [Asyncer](https://asyncer.tiangolo.com/)
+
+`annotated-doc` is supported by [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc), which powers reference documentation like the one in the [FastAPI Reference](https://fastapi.tiangolo.com/reference/).
+
+## Reasons not to use `annotated-doc`
+
+You are already comfortable with one of the existing docstring formats, like:
+
+* Sphinx
+* numpydoc
+* Google
+* Keras
+
+Your team is already comfortable using them.
+
+You prefer having the documentation about parameters all together in a docstring, separated from the code defining them.
+
+You care about a specific set of users, using one specific editor, and that editor already has support for the specific docstring format you use.
+
+## Reasons to use `annotated-doc`
+
+* No micro-syntax to learn for newcomers, it’s **just Python** syntax.
+* **Editing** would be already fully supported by default by any editor (current or future) supporting Python syntax, including syntax errors, syntax highlighting, etc.
+* **Rendering** would be relatively straightforward to implement by static tools (tools that don't need runtime execution), as the information can be extracted from the AST they normally already create.
+* **Deduplication of information**: the name of a parameter would be defined in a single place, not duplicated inside of a docstring.
+* **Elimination** of the possibility of having **inconsistencies** when removing a parameter or class variable and **forgetting to remove** its documentation.
+* **Minimization** of the probability of adding a new parameter or class variable and **forgetting to add its documentation**.
+* **Elimination** of the possibility of having **inconsistencies** between the **name** of a parameter in the **signature** and the name in the docstring when it is renamed.
+* **Access** to the documentation string for each symbol at **runtime**, including existing (older) Python versions.
+* A more formalized way to document other symbols, like type aliases, that could use Annotated.
+* **Support** for apps using FastAPI, Typer and others.
+* **AI Accessibility**: AI tools will have an easier way understanding each parameter as the distance from documentation to parameter is much closer.
+
+## History
+
+I ([@tiangolo](https://github.com/tiangolo)) originally wanted for this to be part of the Python standard library (in [PEP 727](https://peps.python.org/pep-0727/)), but the proposal was withdrawn as there was a fair amount of negative feedback and opposition.
+
+The conclusion was that this was better done as an external effort, in a third-party library.
+
+So, here it is, with a simpler approach, as a third-party library, in a way that can be used by others, starting with FastAPI and friends.
+
+## License
+
+This project is licensed under the terms of the MIT license.
diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/RECORD b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/RECORD
new file mode 100644
index 000000000..83ce62b75
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/RECORD
@@ -0,0 +1,11 @@
+annotated_doc-0.0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+annotated_doc-0.0.5.dist-info/METADATA,sha256=jCTXDF2RU9IhIzU6kGBC_XYD3y0VwhlikLD4qWJVjEY,6516
+annotated_doc-0.0.5.dist-info/RECORD,,
+annotated_doc-0.0.5.dist-info/WHEEL,sha256=VP-D4TPS230sME9Z3vb3INXvo1yt0924YRm5AOsk_dE,90
+annotated_doc-0.0.5.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
+annotated_doc-0.0.5.dist-info/licenses/LICENSE,sha256=__Fwd5pqy_ZavbQFwIfxzuF4ZpHkqWpANFF-SlBKDN8,1086
+annotated_doc/__init__.py,sha256=UqEV5evgeySQ5yvJQwfwTqwvMGfIwbxRPP4gf-o2-LA,52
+annotated_doc/__pycache__/__init__.cpython-311.pyc,,
+annotated_doc/__pycache__/main.cpython-311.pyc,,
+annotated_doc/main.py,sha256=5Zfvxv80SwwLqpRW73AZyZyiM4bWma9QWRbp_cgD20s,1075
+annotated_doc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/WHEEL b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/WHEEL
new file mode 100644
index 000000000..e651d8efb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: pdm-backend (2.4.9)
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/entry_points.txt b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/entry_points.txt
new file mode 100644
index 000000000..c3ad4726d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/entry_points.txt
@@ -0,0 +1,4 @@
+[console_scripts]
+
+[gui_scripts]
+
diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/licenses/LICENSE
new file mode 100644
index 000000000..7a254464c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.5.dist-info/licenses/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2025 Sebastián Ramírez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/venv/lib/python3.11/site-packages/annotated_doc/__init__.py b/venv/lib/python3.11/site-packages/annotated_doc/__init__.py
new file mode 100644
index 000000000..d6f729253
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc/__init__.py
@@ -0,0 +1,3 @@
+from .main import Doc as Doc
+
+__version__ = "0.0.5"
diff --git a/venv/lib/python3.11/site-packages/annotated_doc/main.py b/venv/lib/python3.11/site-packages/annotated_doc/main.py
new file mode 100644
index 000000000..7063c59e4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_doc/main.py
@@ -0,0 +1,36 @@
+class Doc:
+ """Define the documentation of a type annotation using `Annotated`, to be
+ used in class attributes, function and method parameters, return values,
+ and variables.
+
+ The value should be a positional-only string literal to allow static tools
+ like editors and documentation generators to use it.
+
+ This complements docstrings.
+
+ The string value passed is available in the attribute `documentation`.
+
+ Example:
+
+ ```Python
+ from typing import Annotated
+ from annotated_doc import Doc
+
+ def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None:
+ print(f"Hi, {name}!")
+ ```
+ """
+
+ def __init__(self, documentation: str, /) -> None:
+ self.documentation = documentation
+
+ def __repr__(self) -> str:
+ return f"Doc({self.documentation!r})"
+
+ def __hash__(self) -> int:
+ return hash(self.documentation)
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, Doc):
+ return NotImplemented
+ return self.documentation == other.documentation
diff --git a/venv/lib/python3.11/site-packages/annotated_doc/py.typed b/venv/lib/python3.11/site-packages/annotated_doc/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/INSTALLER
new file mode 100644
index 000000000..a1b589e38
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/METADATA b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/METADATA
new file mode 100644
index 000000000..eb5a08847
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/METADATA
@@ -0,0 +1,295 @@
+Metadata-Version: 2.4
+Name: annotated-types
+Version: 0.8.0
+Summary: Reusable constraint types to use with typing.Annotated
+Project-URL: Homepage, https://github.com/annotated-types/annotated-types
+Project-URL: Source, https://github.com/annotated-types/annotated-types
+Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases
+Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds
+License-Expression: MIT
+License-File: LICENSE
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Console
+Classifier: Environment :: MacOS X
+Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Information Technology
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Operating System :: Unix
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Typing :: Typed
+Requires-Python: >=3.10
+Description-Content-Type: text/markdown
+
+# annotated-types
+
+[](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
+[](https://pypi.python.org/pypi/annotated-types)
+[](https://github.com/annotated-types/annotated-types)
+[](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
+
+[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
+adding context-specific metadata to existing types, and specifies that
+`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
+logic for `x`.
+
+This package provides metadata objects which can be used to represent common
+constraints such as upper and lower bounds on scalar values and collection sizes,
+a `Predicate` marker for runtime checks, and
+descriptions of how we intend these metadata to be interpreted. In some cases,
+we also note alternative representations which do not require this package.
+
+## Install
+
+```bash
+pip install annotated-types
+```
+
+## Examples
+
+```python
+from typing import Annotated
+from annotated_types import Gt, Len, Predicate
+
+class MyClass:
+ age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
+ # Invalid: 17, 18, "19", 19.0, ...
+ factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
+ # Invalid: 4, 8, -2, 5.0, "prime", ...
+
+ my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
+ # Invalid: (1, 2), ["abc"], [0] * 20
+```
+
+## Documentation
+
+_While `annotated-types` avoids runtime checks for performance, users should not
+construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
+Downstream implementors may choose to raise an error, emit a warning, silently ignore
+a metadata item, etc., if the metadata objects described below are used with an
+incompatible type - or for any other reason!_
+
+### Gt, Ge, Lt, Le
+
+Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
+dates, times, strings, sets, etc. Note that the boundary value need not be of the
+same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
+is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
+
+We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
+as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
+the `annotated-types` package.
+
+To be explicit, these types have the following meanings:
+
+* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
+* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
+* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
+* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
+
+### Interval
+
+`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
+metadata object. `None` attributes should be ignored, and non-`None` attributes
+treated as per the single bounds above.
+
+### MultipleOf
+
+`MultipleOf(multiple_of=x)` might be interpreted in two ways:
+
+1. Python semantics, implying `value % multiple_of == 0`, or
+2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
+ where `int(value / multiple_of) == value / multiple_of`.
+
+We encourage users to be aware of these two common interpretations and their
+distinct behaviours, especially since very large or non-integer numbers make
+it easy to cause silent data corruption due to floating-point imprecision.
+
+We encourage libraries to carefully document which interpretation they implement.
+
+### MinLen, MaxLen, Len
+
+`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
+
+As well as `Len()` which can optionally include upper and lower bounds, we also
+provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
+and `Len(max_length=y)` respectively.
+
+`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
+
+Examples of usage:
+
+* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10)]`) - list must have a length of 10 or less
+* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
+* `Annotated[list, MinLen(3)]` (or `Annotated[list, Len(min_length=3)]`) - list must have a length of 3 or more
+* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
+* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
+
+#### Changed in v0.4.0
+
+* `min_inclusive` has been renamed to `min_length`, no change in meaning
+* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
+* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
+ meaning of the upper bound in slices vs. `Len`
+
+See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
+
+### Timezone
+
+`Timezone` can be used with a `datetime` or a `time` to express which timezones
+are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
+`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
+expresses that any timezone-aware datetime is allowed. You may also pass a specific
+timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects)
+object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only
+allow a specific timezone, though we note that this is often a symptom of fragile design.
+
+#### Changed in v0.x.x
+
+* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of
+ `timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries.
+
+### Unit
+
+`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of
+a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]`
+would be a float representing a velocity in meters per second.
+
+Please note that `annotated_types` itself makes no attempt to parse or validate
+the unit string in any way. That is left entirely to downstream libraries,
+such as [`pint`](https://pint.readthedocs.io) or
+[`astropy.units`](https://docs.astropy.org/en/stable/units/).
+
+An example of how a library might use this metadata:
+
+```python
+from annotated_types import Unit
+from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args
+
+# given a type annotated with a unit:
+Meters = Annotated[float, Unit("m")]
+
+
+# you can cast the annotation to a specific unit type with any
+# callable that accepts a string and returns the desired type
+T = TypeVar("T")
+def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
+ if get_origin(tp) is Annotated:
+ for arg in get_args(tp):
+ if isinstance(arg, Unit):
+ return unit_cls(arg.unit)
+ return None
+
+
+# using `pint`
+import pint
+pint_unit = cast_unit(Meters, pint.Unit)
+
+
+# using `astropy.units`
+import astropy.units as u
+astropy_unit = cast_unit(Meters, u.Unit)
+```
+
+### Predicate
+
+`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
+Users should prefer the statically inspectable metadata above, but if you need
+the full power and flexibility of arbitrary runtime predicates... here it is.
+
+For some common constraints, we provide generic types:
+
+* `LowerCase = Annotated[T, Predicate(str.islower)]`
+* `UpperCase = Annotated[T, Predicate(str.isupper)]`
+* `IsDigit = Annotated[T, Predicate(str.isdigit)]`
+* `IsFinite = Annotated[T, Predicate(math.isfinite)]`
+* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]`
+* `IsNan = Annotated[T, Predicate(math.isnan)]`
+* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]`
+* `IsInfinite = Annotated[T, Predicate(math.isinf)]`
+* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]`
+
+so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer
+(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`.
+
+Some libraries might have special logic to handle known or understandable predicates,
+for example by checking for `str.isdigit` and using its presence to both call custom
+logic to enforce digit-only strings, and customise some generated external schema.
+Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
+favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
+
+To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
+
+We do not specify what behaviour should be expected for predicates that raise
+an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
+skip invalid constraints, or statically raise an error; or it might try calling it
+and then propagate or discard the resulting
+`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
+exception. We encourage libraries to document the behaviour they choose.
+
+### Doc
+
+`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
+
+It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
+
+It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
+
+This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
+
+### Integrating downstream types with `GroupedMetadata`
+
+Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
+This can help reduce verbosity and cognitive overhead for users.
+For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
+
+```python
+from dataclasses import dataclass
+from typing import Iterator
+from annotated_types import GroupedMetadata, Ge
+
+@dataclass
+class Field(GroupedMetadata):
+ ge: int | None = None
+ description: str | None = None
+
+ def __iter__(self) -> Iterator[object]:
+ # Iterating over a GroupedMetadata object should yield annotated-types
+ # constraint metadata objects which describe it as fully as possible,
+ # and may include other unknown objects too.
+ if self.ge is not None:
+ yield Ge(self.ge)
+ if self.description is not None:
+ yield Description(self.description)
+```
+
+Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
+
+Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
+
+Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
+
+### Consuming metadata
+
+We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
+
+It is up to the implementer to determine how this metadata is used.
+You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
+
+## Design & History
+
+This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
+and Hypothesis, with the goal of making it as easy as possible for end-users to
+provide more informative annotations for use by runtime libraries.
+
+It is deliberately minimal, and following PEP-593 allows considerable downstream
+discretion in what (if anything!) they choose to support. Nonetheless, we expect
+that staying simple and covering _only_ the most common use-cases will give users
+and maintainers the best experience we can. If you'd like more constraints for your
+types - follow our lead, by defining them and documenting them downstream!
diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/RECORD b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/RECORD
new file mode 100644
index 000000000..744476917
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/RECORD
@@ -0,0 +1,10 @@
+annotated_types-0.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+annotated_types-0.8.0.dist-info/METADATA,sha256=YUmFsnj2Abjvhj-CeDuZHMOtpLifWS63HHxIxfLvIpg,15009
+annotated_types-0.8.0.dist-info/RECORD,,
+annotated_types-0.8.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
+annotated_types-0.8.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
+annotated_types/__init__.py,sha256=pxBKTUObJ6n3T8C-I2ubobeDHmBEAmgCogWrwSmKm8g,13273
+annotated_types/__pycache__/__init__.cpython-311.pyc,,
+annotated_types/__pycache__/test_cases.cpython-311.pyc,,
+annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+annotated_types/test_cases.py,sha256=2GdHKstXuBzpf3iXKCjmvFZpyk2zP9Hm9kXGGDhGBo4,6310
diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/WHEEL b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/WHEEL
new file mode 100644
index 000000000..7401812e1
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: hatchling 1.31.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/licenses/LICENSE
new file mode 100644
index 000000000..d99323a99
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_types-0.8.0.dist-info/licenses/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2022 the contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/venv/lib/python3.11/site-packages/annotated_types/__init__.py b/venv/lib/python3.11/site-packages/annotated_types/__init__.py
new file mode 100644
index 000000000..dcb35c54a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_types/__init__.py
@@ -0,0 +1,416 @@
+import math
+import types
+from collections.abc import Callable, Iterator
+from dataclasses import dataclass
+from datetime import tzinfo
+from types import EllipsisType
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Any,
+ Literal,
+ Protocol,
+ SupportsFloat,
+ SupportsIndex,
+ TypeVar,
+ Union,
+ runtime_checkable,
+)
+
+__all__ = (
+ 'BaseMetadata',
+ 'GroupedMetadata',
+ 'Gt',
+ 'Ge',
+ 'Lt',
+ 'Le',
+ 'Interval',
+ 'MultipleOf',
+ 'MinLen',
+ 'MaxLen',
+ 'Len',
+ 'Timezone',
+ 'Predicate',
+ 'LowerCase',
+ 'UpperCase',
+ 'IsDigits',
+ 'IsFinite',
+ 'IsNotFinite',
+ 'IsNan',
+ 'IsNotNan',
+ 'IsInfinite',
+ 'IsNotInfinite',
+ 'doc',
+ 'DocInfo',
+ '__version__',
+)
+
+__version__ = '0.8.0'
+
+
+T = TypeVar('T')
+
+
+# arguments that start with __ are considered
+# positional only
+# see https://peps.python.org/pep-0484/#positional-only-arguments
+
+
+class SupportsGt(Protocol):
+ def __gt__(self: T, __other: T) -> bool:
+ ...
+
+
+class SupportsGe(Protocol):
+ def __ge__(self: T, __other: T) -> bool:
+ ...
+
+
+class SupportsLt(Protocol):
+ def __lt__(self: T, __other: T) -> bool:
+ ...
+
+
+class SupportsLe(Protocol):
+ def __le__(self: T, __other: T) -> bool:
+ ...
+
+
+class SupportsMod(Protocol):
+ def __mod__(self: T, __other: T) -> T:
+ ...
+
+
+class SupportsDiv(Protocol):
+ def __div__(self: T, __other: T) -> T:
+ ...
+
+
+class BaseMetadata:
+ """Base class for all metadata.
+
+ This exists mainly so that implementers
+ can do `isinstance(..., BaseMetadata)` while traversing field annotations.
+ """
+
+ __slots__ = ()
+
+
+@dataclass(frozen=True, slots=True)
+class Gt(BaseMetadata):
+ """Gt(gt=x) implies that the value must be greater than x.
+
+ It can be used with any type that supports the ``>`` operator,
+ including numbers, dates and times, strings, sets, and so on.
+ """
+
+ gt: SupportsGt
+
+
+@dataclass(frozen=True, slots=True)
+class Ge(BaseMetadata):
+ """Ge(ge=x) implies that the value must be greater than or equal to x.
+
+ It can be used with any type that supports the ``>=`` operator,
+ including numbers, dates and times, strings, sets, and so on.
+ """
+
+ ge: SupportsGe
+
+
+@dataclass(frozen=True, slots=True)
+class Lt(BaseMetadata):
+ """Lt(lt=x) implies that the value must be less than x.
+
+ It can be used with any type that supports the ``<`` operator,
+ including numbers, dates and times, strings, sets, and so on.
+ """
+
+ lt: SupportsLt
+
+
+@dataclass(frozen=True, slots=True)
+class Le(BaseMetadata):
+ """Le(le=x) implies that the value must be less than or equal to x.
+
+ It can be used with any type that supports the ``<=`` operator,
+ including numbers, dates and times, strings, sets, and so on.
+ """
+
+ le: SupportsLe
+
+
+@runtime_checkable
+class GroupedMetadata(Protocol):
+ """A grouping of multiple objects, like typing.Unpack.
+
+ `GroupedMetadata` on its own is not metadata and has no meaning.
+ All of the constraints and metadata should be fully expressable
+ in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
+
+ Concrete implementations should override `GroupedMetadata.__iter__()`
+ to add their own metadata.
+ For example:
+
+ >>> @dataclass
+ >>> class Field(GroupedMetadata):
+ >>> gt: float | None = None
+ >>> description: str | None = None
+ ...
+ >>> def __iter__(self) -> Iterable[object]:
+ >>> if self.gt is not None:
+ >>> yield Gt(self.gt)
+ >>> if self.description is not None:
+ >>> yield Description(self.gt)
+
+ Also see the implementation of `Interval` below for an example.
+
+ Parsers should recognize this and unpack it so that it can be used
+ both with and without unpacking:
+
+ - `Annotated[int, Field(...)]` (parser must unpack Field)
+ - `Annotated[int, *Field(...)]` (PEP-646)
+ """ # noqa: trailing-whitespace
+
+ @property
+ def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
+ return True
+
+ def __iter__(self) -> Iterator[object]:
+ ...
+
+ if not TYPE_CHECKING:
+ __slots__ = () # allow subclasses to use slots
+
+ def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
+ # Basic ABC like functionality without the complexity of an ABC
+ super().__init_subclass__(*args, **kwargs)
+ if cls.__iter__ is GroupedMetadata.__iter__:
+ raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
+
+ def __iter__(self) -> Iterator[object]: # noqa: F811
+ raise NotImplementedError # more helpful than "None has no attribute..." type errors
+
+
+@dataclass(frozen=True, kw_only=True, slots=True)
+class Interval(GroupedMetadata):
+ """Interval can express inclusive or exclusive bounds with a single object.
+
+ It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
+ are interpreted the same way as the single-bound constraints.
+ """
+
+ gt: SupportsGt | None = None
+ ge: SupportsGe | None = None
+ lt: SupportsLt | None = None
+ le: SupportsLe | None = None
+
+ def __iter__(self) -> Iterator[BaseMetadata]:
+ """Unpack an Interval into zero or more single-bounds."""
+ if self.gt is not None:
+ yield Gt(self.gt)
+ if self.ge is not None:
+ yield Ge(self.ge)
+ if self.lt is not None:
+ yield Lt(self.lt)
+ if self.le is not None:
+ yield Le(self.le)
+
+
+@dataclass(frozen=True, slots=True)
+class MultipleOf(BaseMetadata):
+ """MultipleOf(multiple_of=x) might be interpreted in two ways:
+
+ 1. Python semantics, implying ``value % multiple_of == 0``, or
+ 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
+
+ We encourage users to be aware of these two common interpretations,
+ and libraries to carefully document which they implement.
+ """
+
+ multiple_of: SupportsDiv | SupportsMod
+
+
+@dataclass(frozen=True, slots=True)
+class MinLen(BaseMetadata):
+ """
+ MinLen() implies minimum inclusive length,
+ e.g. ``len(value) >= min_length``.
+ """
+
+ min_length: Annotated[int, Ge(0)]
+
+
+@dataclass(frozen=True, slots=True)
+class MaxLen(BaseMetadata):
+ """
+ MaxLen() implies maximum inclusive length,
+ e.g. ``len(value) <= max_length``.
+ """
+
+ max_length: Annotated[int, Ge(0)]
+
+
+@dataclass(frozen=True, slots=True)
+class Len(GroupedMetadata):
+ """
+ Len() implies that ``min_length <= len(value) <= max_length``.
+
+ Upper bound may be omitted or ``None`` to indicate no upper length bound.
+ """
+
+ min_length: Annotated[int, Ge(0)] = 0
+ max_length: Annotated[int, Ge(0)] | None = None
+
+ def __iter__(self) -> Iterator[BaseMetadata]:
+ """Unpack a Len into zero or more single-bounds."""
+ if self.min_length > 0:
+ yield MinLen(self.min_length)
+ if self.max_length is not None:
+ yield MaxLen(self.max_length)
+
+
+@dataclass(frozen=True, slots=True)
+class Timezone(BaseMetadata):
+ """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
+
+ ``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
+ ``Timezone(...)`` (the ellipsis literal) expresses that the datetime must be
+ tz-aware but any timezone is allowed.
+
+ You may also pass a specific timezone string or tzinfo object such as
+ ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
+ you only allow a specific timezone, though we note that this is often
+ a symptom of poor design.
+ """
+
+ tz: str | tzinfo | EllipsisType | None
+
+
+@dataclass(frozen=True, slots=True)
+class Unit(BaseMetadata):
+ """Indicates that the value is a physical quantity with the specified unit.
+
+ It is intended for usage with numeric types, where the value represents the
+ magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]``
+ or ``speed: Annotated[float, Unit('m/s')]``.
+
+ Interpretation of the unit string is left to the discretion of the consumer.
+ It is suggested to follow conventions established by python libraries that work
+ with physical quantities, such as
+
+ - ``pint`` :
+ - ``astropy.units``:
+
+ For indicating a quantity with a certain dimensionality but without a specific unit
+ it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`.
+ Note, however, ``annotated_types`` itself makes no use of the unit string.
+ """
+
+ unit: str
+
+
+@dataclass(frozen=True, slots=True)
+class Predicate(BaseMetadata):
+ """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
+
+ Users should prefer statically inspectable metadata, but if you need the full
+ power and flexibility of arbitrary runtime predicates... here it is.
+
+ We provide a few predefined predicates for common string constraints:
+ ``LowerCase = Predicate(str.islower)``, ``UpperCase = Predicate(str.isupper)``, and
+ ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which
+ can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
+
+ Some libraries might have special logic to handle certain predicates, e.g. by
+ checking for `str.isdigit` and using its presence to both call custom logic to
+ enforce digit-only strings, and customise some generated external schema.
+
+ We do not specify what behaviour should be expected for predicates that raise
+ an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
+ skip invalid constraints, or statically raise an error; or it might try calling it
+ and then propagate or discard the resulting exception.
+ """
+
+ func: Callable[[Any], bool]
+
+ def __repr__(self) -> str:
+ if getattr(self.func, "__name__", "") == "":
+ return f"{self.__class__.__name__}({self.func!r})"
+ if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and (
+ namespace := getattr(self.func.__self__, "__name__", None)
+ ):
+ return f"{self.__class__.__name__}({namespace}.{self.func.__name__})"
+ if isinstance(self.func, type(str.isascii)): # method descriptor
+ return f"{self.__class__.__name__}({self.func.__qualname__})"
+ return f"{self.__class__.__name__}({self.func.__name__})"
+
+
+@dataclass
+class Not:
+ func: Callable[[Any], bool]
+
+ def __call__(self, __v: Any) -> bool:
+ return not self.func(__v)
+
+
+_StrType = TypeVar("_StrType", bound=str)
+
+LowerCase = Annotated[_StrType, Predicate(str.islower)]
+"""
+Return True if the string is a lowercase string, False otherwise.
+
+A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
+""" # noqa: E501
+UpperCase = Annotated[_StrType, Predicate(str.isupper)]
+"""
+Return True if the string is an uppercase string, False otherwise.
+
+A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
+""" # noqa: E501
+IsDigit = Annotated[_StrType, Predicate(str.isdigit)]
+IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63
+"""
+Return True if the string is a digit string, False otherwise.
+
+A string is a digit string if all characters in the string are digits and there is at least one character in the string.
+""" # noqa: E501
+IsAscii = Annotated[_StrType, Predicate(str.isascii)]
+"""
+Return True if all characters in the string are ASCII, False otherwise.
+
+ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
+"""
+
+_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
+IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
+"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
+IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
+"""Return True if x is one of infinity or NaN, and False otherwise"""
+IsNan = Annotated[_NumericType, Predicate(math.isnan)]
+"""Return True if x is a NaN (not a number), and False otherwise."""
+IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
+"""Return True if x is anything but NaN (not a number), and False otherwise."""
+IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
+"""Return True if x is a positive or negative infinity, and False otherwise."""
+IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
+"""Return True if x is neither a positive or negative infinity, and False otherwise."""
+
+try:
+ # PEP 727 – Documentation in Annotated Metadata
+ from typing_extensions import Doc # type: ignore[attr-defined]
+except ImportError:
+
+ @dataclass(frozen=True, slots=True)
+ class Doc: # type: ignore [no-redef]
+ """ "
+ The return value of doc(), mainly to be used by tools that want to extract the
+ Annotated documentation at runtime.
+ """
+
+ documentation: str
+ """The documentation string passed to doc()."""
+
+
+DocInfo = Doc # backwards compatibility
+doc = Doc
diff --git a/venv/lib/python3.11/site-packages/annotated_types/py.typed b/venv/lib/python3.11/site-packages/annotated_types/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/annotated_types/test_cases.py b/venv/lib/python3.11/site-packages/annotated_types/test_cases.py
new file mode 100644
index 000000000..fba3c2573
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/annotated_types/test_cases.py
@@ -0,0 +1,146 @@
+import math
+from collections.abc import Iterable, Iterator
+from datetime import date, datetime, timedelta, timezone
+from decimal import Decimal
+from typing import Annotated, Any, NamedTuple
+
+import annotated_types as at
+
+
+class Case(NamedTuple):
+ """
+ A test case for `annotated_types`.
+ """
+
+ annotation: Any
+ valid_cases: Iterable[Any]
+ invalid_cases: Iterable[Any]
+
+
+def cases() -> Iterable[Case]:
+ # Gt, Ge, Lt, Le
+ yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
+ yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
+ yield Case(
+ Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
+ [datetime(2000, 1, 2), datetime(2000, 1, 3)],
+ [datetime(2000, 1, 1), datetime(1999, 12, 31)],
+ )
+ yield Case(
+ Annotated[datetime, at.Gt(date(2000, 1, 1))],
+ [date(2000, 1, 2), date(2000, 1, 3)],
+ [date(2000, 1, 1), date(1999, 12, 31)],
+ )
+ yield Case(
+ Annotated[datetime, at.Gt(Decimal('1.123'))],
+ [Decimal('1.1231'), Decimal('123')],
+ [Decimal('1.123'), Decimal('0')],
+ )
+
+ yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
+ yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
+ yield Case(
+ Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
+ [datetime(2000, 1, 2), datetime(2000, 1, 3)],
+ [datetime(1998, 1, 1), datetime(1999, 12, 31)],
+ )
+
+ yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
+ yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
+ yield Case(
+ Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
+ [datetime(1999, 12, 31), datetime(1999, 12, 31)],
+ [datetime(2000, 1, 2), datetime(2000, 1, 3)],
+ )
+
+ yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
+ yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
+ yield Case(
+ Annotated[datetime, at.Le(datetime(2000, 1, 1))],
+ [datetime(2000, 1, 1), datetime(1999, 12, 31)],
+ [datetime(2000, 1, 2), datetime(2000, 1, 3)],
+ )
+
+ # Interval
+ yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
+ yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
+ yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
+ yield Case(
+ Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
+ [datetime(2000, 1, 2), datetime(2000, 1, 3)],
+ [datetime(2000, 1, 1), datetime(2000, 1, 4)],
+ )
+
+ yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
+ yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
+
+ # lengths
+
+ yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
+ yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
+ yield Case(Annotated[list[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
+ yield Case(Annotated[list[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
+
+ yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
+ yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
+ yield Case(Annotated[list[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
+ yield Case(Annotated[list[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
+
+ yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
+ yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
+
+ yield Case(Annotated[dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
+ yield Case(Annotated[set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
+ yield Case(Annotated[tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
+
+ # Timezone
+
+ yield Case(
+ Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
+ )
+ yield Case(
+ Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
+ )
+ yield Case(
+ Annotated[datetime, at.Timezone(timezone.utc)],
+ [datetime(2000, 1, 1, tzinfo=timezone.utc)],
+ [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
+ )
+ yield Case(
+ Annotated[datetime, at.Timezone('Europe/London')],
+ [datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
+ [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
+ )
+
+ # Quantity
+
+ yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m'))
+
+ # predicate types
+
+ yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
+ yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
+ yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2'])
+ yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
+
+ yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
+
+ yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
+ yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
+ yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
+ yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
+ yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
+ yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
+
+ # check stacked predicates
+ yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
+
+ # doc
+ yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
+
+ # custom GroupedMetadata
+ class MyCustomGroupedMetadata(at.GroupedMetadata):
+ def __iter__(self) -> Iterator[at.Predicate]:
+ yield at.Predicate(lambda x: float(x).is_integer())
+
+ yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])
diff --git a/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/INSTALLER
new file mode 100644
index 000000000..a1b589e38
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/METADATA b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/METADATA
new file mode 100644
index 000000000..13734ecac
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/METADATA
@@ -0,0 +1,108 @@
+Metadata-Version: 2.4
+Name: anyio
+Version: 4.15.1
+Summary: High-level concurrency and networking framework on top of asyncio or Trio
+Author-email: Alex Grönholm
+License-Expression: MIT
+Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/
+Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html
+Project-URL: Source code, https://github.com/agronholm/anyio
+Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Framework :: AnyIO
+Classifier: Typing :: Typed
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
+Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
+Requires-Python: >=3.10
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11"
+Requires-Dist: idna>=2.8
+Requires-Dist: typing_extensions>=4.16.0; python_version < "3.15"
+Provides-Extra: trio
+Requires-Dist: trio>=0.32.0; extra == "trio"
+Dynamic: license-file
+
+.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg
+ :target: https://github.com/agronholm/anyio/actions/workflows/test.yml
+ :alt: Build Status
+.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master
+ :target: https://coveralls.io/github/agronholm/anyio?branch=master
+ :alt: Code Coverage
+.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest
+ :target: https://anyio.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation
+.. image:: https://badges.gitter.im/gitterHQ/gitter.svg
+ :target: https://gitter.im/python-trio/AnyIO
+ :alt: Gitter chat
+.. image:: https://tidelift.com/badges/package/pypi/anyio
+ :target: https://tidelift.com/subscription/pkg/pypi-anyio
+ :alt: Tidelift
+
+AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or
+Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony
+with the native SC of Trio itself.
+
+Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or
+Trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full
+refactoring necessary. It will blend in with the native libraries of your chosen backend.
+
+To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it
+`here `_.
+
+Documentation
+-------------
+
+View full documentation at: https://anyio.readthedocs.io/
+
+Features
+--------
+
+AnyIO offers the following functionality:
+
+* Task groups (nurseries_ in trio terminology)
+* High-level networking (TCP, UDP and UNIX sockets)
+
+ * `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python
+ 3.8)
+ * async/await style UDP sockets (unlike asyncio where you still have to use Transports and
+ Protocols)
+
+* A versatile API for byte streams and object streams
+* Inter-task synchronization and communication (locks, conditions, events, semaphores, object
+ streams and futures)
+* Worker threads
+* Subprocesses
+* Subinterpreter support for code parallelization (on Python 3.13 and later)
+* Asynchronous file I/O (using worker threads)
+* Signal handling
+* Asynchronous versions of the functools_ and itertools_ modules
+
+AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures.
+It even works with the popular Hypothesis_ library.
+
+.. _asyncio: https://docs.python.org/3/library/asyncio.html
+.. _Trio: https://github.com/python-trio/trio
+.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency
+.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
+.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs
+.. _pytest: https://docs.pytest.org/en/latest/
+.. _functools: https://docs.python.org/3/library/functools.html
+.. _itertools: https://docs.python.org/3/library/itertools.html
+.. _Hypothesis: https://hypothesis.works/
+
+Security contact information
+----------------------------
+
+To report a security vulnerability, please use the `Tidelift security contact`_.
+Tidelift will coordinate the fix and disclosure.
+
+.. _Tidelift security contact: https://tidelift.com/security
diff --git a/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/RECORD b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/RECORD
new file mode 100644
index 000000000..76924cc5c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/RECORD
@@ -0,0 +1,100 @@
+anyio-4.15.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+anyio-4.15.1.dist-info/METADATA,sha256=BDrstXGvcYnopsUo9djjJUXxss36r3YdUebVtn6jyoE,4733
+anyio-4.15.1.dist-info/RECORD,,
+anyio-4.15.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
+anyio-4.15.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
+anyio-4.15.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
+anyio-4.15.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
+anyio/__init__.py,sha256=OfroYRjIUw_JFj0Zwh1JAaajMQg4VqtoAt0XoOHDRwE,7602
+anyio/__pycache__/__init__.cpython-311.pyc,,
+anyio/__pycache__/_lazyimport.cpython-311.pyc,,
+anyio/__pycache__/from_thread.cpython-311.pyc,,
+anyio/__pycache__/functools.cpython-311.pyc,,
+anyio/__pycache__/itertools.cpython-311.pyc,,
+anyio/__pycache__/lowlevel.cpython-311.pyc,,
+anyio/__pycache__/pytest_plugin.cpython-311.pyc,,
+anyio/__pycache__/to_interpreter.cpython-311.pyc,,
+anyio/__pycache__/to_process.cpython-311.pyc,,
+anyio/__pycache__/to_thread.cpython-311.pyc,,
+anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/_backends/__pycache__/__init__.cpython-311.pyc,,
+anyio/_backends/__pycache__/_asyncio.cpython-311.pyc,,
+anyio/_backends/__pycache__/_trio.cpython-311.pyc,,
+anyio/_backends/_asyncio.py,sha256=Vqy7O1ymmEpxTiPzehSuBCVTziGALNV-BbsIRaKXsg4,107767
+anyio/_backends/_trio.py,sha256=Gs8XzX0IeBvje8IEZtJmpNylKCUAChKv7I_YZi3wlmg,45715
+anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/_core/__pycache__/__init__.cpython-311.pyc,,
+anyio/_core/__pycache__/_asyncio_selector_thread.cpython-311.pyc,,
+anyio/_core/__pycache__/_concurrency_utils.cpython-311.pyc,,
+anyio/_core/__pycache__/_contextmanagers.cpython-311.pyc,,
+anyio/_core/__pycache__/_eventloop.cpython-311.pyc,,
+anyio/_core/__pycache__/_exceptions.cpython-311.pyc,,
+anyio/_core/__pycache__/_fileio.cpython-311.pyc,,
+anyio/_core/__pycache__/_futures.cpython-311.pyc,,
+anyio/_core/__pycache__/_resources.cpython-311.pyc,,
+anyio/_core/__pycache__/_signals.cpython-311.pyc,,
+anyio/_core/__pycache__/_sockets.cpython-311.pyc,,
+anyio/_core/__pycache__/_streams.cpython-311.pyc,,
+anyio/_core/__pycache__/_subprocesses.cpython-311.pyc,,
+anyio/_core/__pycache__/_synchronization.cpython-311.pyc,,
+anyio/_core/__pycache__/_tasks.cpython-311.pyc,,
+anyio/_core/__pycache__/_tempfile.cpython-311.pyc,,
+anyio/_core/__pycache__/_testing.cpython-311.pyc,,
+anyio/_core/__pycache__/_typedattr.cpython-311.pyc,,
+anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626
+anyio/_core/_concurrency_utils.py,sha256=kvRXMrnDqgQTBkZc-f279-lBnZM4YbSToXqhnAUCfSc,4522
+anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215
+anyio/_core/_eventloop.py,sha256=ByZUeJD9alMfcyTseRo5IzTO0IltEul_Gyq9iqSjqDk,6658
+anyio/_core/_exceptions.py,sha256=B4ZhLkBQT4tI6vShv4-GJui492IJsoIDTw1FQJEkmAI,5618
+anyio/_core/_fileio.py,sha256=q--ft9h5Y_FRLzhAnoYrXf3tOuZ8ZyIN2A9mCfMbZRc,34163
+anyio/_core/_futures.py,sha256=YQq5uhnCnKQMQD62tOLfzEm7dUXY3GQorJdH6ghfgIQ,6177
+anyio/_core/_resources.py,sha256=Ld5duCe9NwgPfI7EIOh3CMIBYQ7rq6ZQEa7CFS0u9Ec,450
+anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016
+anyio/_core/_sockets.py,sha256=C8b-3w5H9n0Fo4g7X8WJvk0m6CGh6NVAAFtieBGHK3g,35289
+anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806
+anyio/_core/_subprocesses.py,sha256=TMBuGNILz-47w5t9vayJ1GpCpDT6LxsMohJDFCewIRc,7938
+anyio/_core/_synchronization.py,sha256=ggsd74QPd846faA2HQeT07ONQ5HQ1S78gy8mvB3qCrw,21658
+anyio/_core/_tasks.py,sha256=J2dhM_Jq3Grsfg_enNWpRz2SxfM15uLt-pEfvoQq3uA,15391
+anyio/_core/_tempfile.py,sha256=IgoBCFaPhrFvsF9KHcCcrnCRn-UqMfw0WRdSDWgxAiI,19726
+anyio/_core/_testing.py,sha256=ZRucLh5kqhiFWAJMMbd8yZ6uIvz0rq3WwL72ldosxAU,2340
+anyio/_core/_typedattr.py,sha256=gt9Dzn-IL0QW14aHPDD8PAtq95FYKNNupHyf37xZGPQ,2607
+anyio/_lazyimport.py,sha256=LCjnYanIfYE9G-Dv_EsqNLDuVRppNicWZjCTfS9-sJU,6082
+anyio/abc/__init__.py,sha256=XIVvCI2c6oyaOcqXe94k2rzU5_s7-ZNnlxGTk34H4T0,3050
+anyio/abc/__pycache__/__init__.cpython-311.pyc,,
+anyio/abc/__pycache__/_eventloop.cpython-311.pyc,,
+anyio/abc/__pycache__/_resources.cpython-311.pyc,,
+anyio/abc/__pycache__/_sockets.cpython-311.pyc,,
+anyio/abc/__pycache__/_streams.cpython-311.pyc,,
+anyio/abc/__pycache__/_subprocesses.cpython-311.pyc,,
+anyio/abc/__pycache__/_tasks.cpython-311.pyc,,
+anyio/abc/__pycache__/_testing.cpython-311.pyc,,
+anyio/abc/_eventloop.py,sha256=0-UwgB351rkocCt8m3fjTmcNx2Fxn52nUoujG5DzMvU,11127
+anyio/abc/_resources.py,sha256=hrb0LXUgHSp3i8G6W8JJchdzKXgg1m0T-L7udXmv2y0,900
+anyio/abc/_sockets.py,sha256=jQWnPX7iDIDyvyvVkbuFhjezw4oI2WggzbW6Vl2-eqs,13162
+anyio/abc/_streams.py,sha256=IMeOPPutFXRbyeAutxV1HJe7FUQZYp2qKrJJpP1DCv0,7620
+anyio/abc/_subprocesses.py,sha256=CDqU1p0HuBS0pXUzBW_hbfhihTJRM67n__7qZmoPU84,2123
+anyio/abc/_tasks.py,sha256=HBPpT72aOy0bwMAxBCKmnXsR328wCYRjYm3iaVw2fQo,7070
+anyio/abc/_testing.py,sha256=A9Y8RAClIPn1A0aNHBN8YoaRy_MKOpbFGEbTHAXgB6U,2145
+anyio/from_thread.py,sha256=iZZbfcNySi4wAcx1_cxAQE4J0YZC88qRMIYqQ6SF9sc,19415
+anyio/functools.py,sha256=T4JS8IXq-x1S0Lbo2owF8l9fza2KypO147QLeyz4cjs,11797
+anyio/itertools.py,sha256=TS7_UCa2Y3cggtaLlWHbjOyf6QhPfQUdPduNW-ddXTE,16255
+anyio/lowlevel.py,sha256=QyWkvfetbCH0mHkqv1yxGwOaiZ7fyxhpZkHWOXPV5JU,6280
+anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/pytest_plugin.py,sha256=oLXyOstvRZW3VVbqIODhZsPt4jSz2UGh44YeG2nJXx0,14516
+anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+anyio/streams/__pycache__/__init__.cpython-311.pyc,,
+anyio/streams/__pycache__/buffered.cpython-311.pyc,,
+anyio/streams/__pycache__/file.cpython-311.pyc,,
+anyio/streams/__pycache__/memory.cpython-311.pyc,,
+anyio/streams/__pycache__/stapled.cpython-311.pyc,,
+anyio/streams/__pycache__/text.cpython-311.pyc,,
+anyio/streams/__pycache__/tls.cpython-311.pyc,,
+anyio/streams/buffered.py,sha256=u7hCD8SNrYHcutG6K5wEiy1F88-pizgjvEFM22Kq2Cw,6746
+anyio/streams/file.py,sha256=6jujI2m-QJITqqKFamrupX_DNsU7y2Fz3omLZxOLuY0,4524
+anyio/streams/memory.py,sha256=rpxNzDMag9PCOynBqN64KyuAfAPB41vFyRP-Npo-lE0,10793
+anyio/streams/stapled.py,sha256=UJ2Rgt0NUmM1VovRNOoZoic960lcbDjb3vhfZg3b_Z0,4845
+anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765
+anyio/streams/tls.py,sha256=fqLmxvULFO9rvxGyJ33VPrOIN7E3eiU1q90BkmKigQ8,15818
+anyio/to_interpreter.py,sha256=ATnoT6kei9wDmQZtXTA3SsY4CK7XKr8Ta4_x5i6pbTw,7100
+anyio/to_process.py,sha256=p6n8j0nreJUl1DEkR_9PL6KRoKtNqFxORTn8qZBvn94,9850
+anyio/to_thread.py,sha256=8MPVrrQq8ttGvV21cSd68rMto5bhxJ0-i9CupXag5Gs,2750
diff --git a/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/WHEEL b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/WHEEL
new file mode 100644
index 000000000..13013d7b4
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: setuptools (84.0.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/entry_points.txt b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/entry_points.txt
new file mode 100644
index 000000000..44dd9bdc3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[pytest11]
+anyio = anyio.pytest_plugin
diff --git a/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/licenses/LICENSE
new file mode 100644
index 000000000..104eebf5a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/licenses/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Alex Grönholm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/top_level.txt
new file mode 100644
index 000000000..c77c069ec
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio-4.15.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+anyio
diff --git a/venv/lib/python3.11/site-packages/anyio/__init__.py b/venv/lib/python3.11/site-packages/anyio/__init__.py
new file mode 100644
index 000000000..30a3a1615
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/__init__.py
@@ -0,0 +1,154 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from ._lazyimport import (
+ fix_package_names,
+ install_lazy_importer,
+ set_deprecated_aliases,
+)
+
+if TYPE_CHECKING or not install_lazy_importer():
+ from ._core._concurrency_utils import amap as amap
+ from ._core._concurrency_utils import as_completed as as_completed
+ from ._core._concurrency_utils import gather as gather
+ from ._core._contextmanagers import (
+ AsyncContextManagerMixin as AsyncContextManagerMixin,
+ )
+ from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin
+ from ._core._eventloop import current_time as current_time
+ from ._core._eventloop import get_all_backends as get_all_backends
+ from ._core._eventloop import get_available_backends as get_available_backends
+ from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class
+ from ._core._eventloop import run as run
+ from ._core._eventloop import sleep as sleep
+ from ._core._eventloop import sleep_forever as sleep_forever
+ from ._core._eventloop import sleep_until as sleep_until
+ from ._core._exceptions import BrokenResourceError as BrokenResourceError
+ from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter
+ from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess
+ from ._core._exceptions import BusyResourceError as BusyResourceError
+ from ._core._exceptions import ClosedResourceError as ClosedResourceError
+ from ._core._exceptions import ConnectionFailed as ConnectionFailed
+ from ._core._exceptions import DelimiterNotFound as DelimiterNotFound
+ from ._core._exceptions import EndOfStream as EndOfStream
+ from ._core._exceptions import FutureAlreadyFinished as FutureAlreadyFinished
+ from ._core._exceptions import FutureCancelled as FutureCancelled
+ from ._core._exceptions import FutureFailed as FutureFailed
+ from ._core._exceptions import FutureNotFinished as FutureNotFinished
+ from ._core._exceptions import IncompleteRead as IncompleteRead
+ from ._core._exceptions import NoEventLoopError as NoEventLoopError
+ from ._core._exceptions import RunFinishedError as RunFinishedError
+ from ._core._exceptions import TaskCancelled as TaskCancelled
+ from ._core._exceptions import TaskFailed as TaskFailed
+ from ._core._exceptions import TaskNotFinished as TaskNotFinished
+ from ._core._exceptions import (
+ TypedAttributeLookupError as TypedAttributeLookupError,
+ )
+ from ._core._exceptions import WouldBlock as WouldBlock
+ from ._core._fileio import AsyncFile as AsyncFile
+ from ._core._fileio import Path as Path
+ from ._core._fileio import open_file as open_file
+ from ._core._fileio import wrap_file as wrap_file
+ from ._core._futures import Future as Future
+ from ._core._resources import aclose_forcefully as aclose_forcefully
+ from ._core._signals import open_signal_receiver as open_signal_receiver
+ from ._core._sockets import TCPConnectable as TCPConnectable
+ from ._core._sockets import UNIXConnectable as UNIXConnectable
+ from ._core._sockets import as_connectable as as_connectable
+ from ._core._sockets import connect_tcp as connect_tcp
+ from ._core._sockets import connect_unix as connect_unix
+ from ._core._sockets import (
+ create_connected_udp_socket as create_connected_udp_socket,
+ )
+ from ._core._sockets import (
+ create_connected_unix_datagram_socket as create_connected_unix_datagram_socket,
+ )
+ from ._core._sockets import create_tcp_listener as create_tcp_listener
+ from ._core._sockets import create_udp_socket as create_udp_socket
+ from ._core._sockets import (
+ create_unix_datagram_socket as create_unix_datagram_socket,
+ )
+ from ._core._sockets import create_unix_listener as create_unix_listener
+ from ._core._sockets import getaddrinfo as getaddrinfo
+ from ._core._sockets import getnameinfo as getnameinfo
+ from ._core._sockets import notify_closing as notify_closing
+ from ._core._sockets import wait_readable as wait_readable
+ from ._core._sockets import wait_socket_readable as wait_socket_readable
+ from ._core._sockets import wait_socket_writable as wait_socket_writable
+ from ._core._sockets import wait_writable as wait_writable
+ from ._core._streams import (
+ create_memory_object_stream as create_memory_object_stream,
+ )
+ from ._core._subprocesses import open_process as open_process
+ from ._core._subprocesses import run_process as run_process
+ from ._core._synchronization import CapacityLimiter as CapacityLimiter
+ from ._core._synchronization import (
+ CapacityLimiterStatistics as CapacityLimiterStatistics,
+ )
+ from ._core._synchronization import Condition as Condition
+ from ._core._synchronization import ConditionStatistics as ConditionStatistics
+ from ._core._synchronization import Event as Event
+ from ._core._synchronization import EventStatistics as EventStatistics
+ from ._core._synchronization import Lock as Lock
+ from ._core._synchronization import LockStatistics as LockStatistics
+ from ._core._synchronization import ResourceGuard as ResourceGuard
+ from ._core._synchronization import Semaphore as Semaphore
+ from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics
+ from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED
+ from ._core._tasks import CancelScope as CancelScope
+ from ._core._tasks import TaskHandle as TaskHandle
+ from ._core._tasks import create_task_group as create_task_group
+ from ._core._tasks import current_effective_deadline as current_effective_deadline
+ from ._core._tasks import fail_after as fail_after
+ from ._core._tasks import fail_at as fail_at
+ from ._core._tasks import move_on_after as move_on_after
+ from ._core._tasks import move_on_at as move_on_at
+ from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile
+ from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile
+ from ._core._tempfile import TemporaryDirectory as TemporaryDirectory
+ from ._core._tempfile import TemporaryFile as TemporaryFile
+ from ._core._tempfile import gettempdir as gettempdir
+ from ._core._tempfile import gettempdirb as gettempdirb
+ from ._core._tempfile import mkdtemp as mkdtemp
+ from ._core._tempfile import mkstemp as mkstemp
+ from ._core._testing import TaskInfo as TaskInfo
+ from ._core._testing import get_current_task as get_current_task
+ from ._core._testing import get_running_tasks as get_running_tasks
+ from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked
+ from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider
+ from ._core._typedattr import TypedAttributeSet as TypedAttributeSet
+ from ._core._typedattr import typed_attribute as typed_attribute
+
+ # ruff: isort: off
+ from . import (
+ abc as abc,
+ )
+ from . import (
+ from_thread as from_thread,
+ )
+ from . import (
+ functools as functools,
+ )
+ from . import (
+ itertools as itertools,
+ )
+ from . import (
+ lowlevel as lowlevel,
+ )
+ from . import (
+ to_interpreter as to_interpreter,
+ )
+ from . import (
+ to_process as to_process,
+ )
+ from . import (
+ to_thread as to_thread,
+ )
+
+ fix_package_names()
+ set_deprecated_aliases(
+ {
+ "BrokenWorkerIntepreter": "anyio.BrokenWorkerInterpreter",
+ }
+ )
diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/__init__.py b/venv/lib/python3.11/site-packages/anyio/_backends/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py b/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py
new file mode 100644
index 000000000..7eba257c9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py
@@ -0,0 +1,3201 @@
+from __future__ import annotations
+
+import array
+import asyncio
+import concurrent.futures
+import math
+import os
+import socket
+import sys
+import threading
+import weakref
+from asyncio import (
+ AbstractEventLoop,
+ CancelledError,
+ all_tasks,
+ create_task,
+ current_task,
+ get_running_loop,
+ sleep,
+)
+from asyncio.base_events import _run_until_complete_cb # type: ignore[attr-defined]
+from collections import OrderedDict, deque
+from collections.abc import (
+ AsyncGenerator,
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Collection,
+ Coroutine,
+ Iterable,
+ Sequence,
+)
+from concurrent.futures import Future
+from contextlib import AbstractContextManager, suppress
+from contextvars import Context, copy_context
+from dataclasses import dataclass, field
+from functools import partial, wraps
+from inspect import (
+ CORO_RUNNING,
+ CORO_SUSPENDED,
+ getcoroutinestate,
+)
+from io import IOBase
+from os import PathLike
+from queue import Queue
+from signal import Signals
+from socket import AddressFamily, SocketKind
+from threading import Thread
+from types import CodeType, TracebackType
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Literal,
+ ParamSpec,
+ TypeVar,
+ cast,
+)
+from weakref import WeakKeyDictionary
+
+from .. import (
+ CapacityLimiterStatistics,
+ EventStatistics,
+ LockStatistics,
+ TaskInfo,
+ abc,
+)
+from .._core._eventloop import (
+ claim_worker_thread,
+ set_current_async_library,
+ threadlocals,
+)
+from .._core._exceptions import (
+ BrokenResourceError,
+ BusyResourceError,
+ ClosedResourceError,
+ EndOfStream,
+ RunFinishedError,
+ WouldBlock,
+)
+from .._core._sockets import convert_ipv6_sockaddr
+from .._core._streams import create_memory_object_stream
+from .._core._synchronization import (
+ CapacityLimiter as BaseCapacityLimiter,
+)
+from .._core._synchronization import Event as BaseEvent
+from .._core._synchronization import Lock as BaseLock
+from .._core._synchronization import (
+ ResourceGuard,
+ SemaphoreStatistics,
+)
+from .._core._synchronization import Semaphore as BaseSemaphore
+from .._core._tasks import CancelScope as BaseCancelScope
+from .._core._tasks import TaskHandle
+from ..abc import (
+ AsyncBackend,
+ IPSockAddrType,
+ SocketListener,
+ UDPPacketType,
+ UNIXDatagramPacketType,
+)
+from ..abc._tasks import call_for_coroutine, get_callable_name, get_coro_name
+from ..lowlevel import RunVar, _run_vars
+
+if TYPE_CHECKING:
+ from _typeshed import FileDescriptorLike
+
+ from ..abc._eventloop import StrOrBytesPath
+ from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
+else:
+ FileDescriptorLike = object
+
+if sys.version_info >= (3, 11):
+ from asyncio import Runner
+ from typing import Self, TypeVarTuple, Unpack
+else:
+ import contextvars
+ import enum
+ import signal
+ from asyncio import coroutines, events, exceptions, tasks
+
+ from exceptiongroup import BaseExceptionGroup
+ from typing_extensions import Self, TypeVarTuple, Unpack
+
+ class _State(enum.Enum):
+ CREATED = "created"
+ INITIALIZED = "initialized"
+ CLOSED = "closed"
+
+ class Runner:
+ # Copied from CPython 3.11
+ def __init__(
+ self,
+ *,
+ debug: bool | None = None,
+ loop_factory: Callable[[], AbstractEventLoop] | None = None,
+ ):
+ self._state = _State.CREATED
+ self._debug = debug
+ self._loop_factory = loop_factory
+ self._loop: AbstractEventLoop | None = None
+ self._context = None
+ self._interrupt_count = 0
+ self._set_event_loop = False
+
+ def __enter__(self) -> Self:
+ self._lazy_init()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.close()
+
+ def close(self) -> None:
+ """Shutdown and close event loop."""
+ loop = self._loop
+ if self._state is not _State.INITIALIZED or loop is None:
+ return
+ try:
+ _cancel_all_tasks(loop)
+ loop.run_until_complete(loop.shutdown_asyncgens())
+ if hasattr(loop, "shutdown_default_executor"):
+ loop.run_until_complete(loop.shutdown_default_executor())
+ else:
+ loop.run_until_complete(_shutdown_default_executor(loop))
+ finally:
+ if self._set_event_loop:
+ events.set_event_loop(None)
+ loop.close()
+ self._loop = None
+ self._state = _State.CLOSED
+
+ def get_loop(self) -> AbstractEventLoop:
+ """Return embedded event loop."""
+ self._lazy_init()
+ return self._loop
+
+ def run(self, coro: Coroutine[T_Retval], *, context=None) -> T_Retval:
+ """Run a coroutine inside the embedded event loop."""
+ if not coroutines.iscoroutine(coro):
+ raise ValueError(f"a coroutine was expected, got {coro!r}")
+
+ if events._get_running_loop() is not None:
+ # fail fast with short traceback
+ raise RuntimeError(
+ "Runner.run() cannot be called from a running event loop"
+ )
+
+ self._lazy_init()
+
+ if context is None:
+ context = self._context
+ task = context.run(self._loop.create_task, coro)
+
+ if (
+ threading.current_thread() is threading.main_thread()
+ and signal.getsignal(signal.SIGINT) is signal.default_int_handler
+ ):
+ sigint_handler = partial(self._on_sigint, main_task=task)
+ try:
+ signal.signal(signal.SIGINT, sigint_handler)
+ except ValueError:
+ # `signal.signal` may throw if `threading.main_thread` does
+ # not support signals (e.g. embedded interpreter with signals
+ # not registered - see gh-91880)
+ sigint_handler = None
+ else:
+ sigint_handler = None
+
+ self._interrupt_count = 0
+ try:
+ return self._loop.run_until_complete(task)
+ except exceptions.CancelledError:
+ if self._interrupt_count > 0:
+ uncancel = getattr(task, "uncancel", None)
+ if uncancel is not None and uncancel() == 0:
+ raise KeyboardInterrupt # noqa: B904
+ raise # CancelledError
+ finally:
+ if (
+ sigint_handler is not None
+ and signal.getsignal(signal.SIGINT) is sigint_handler
+ ):
+ signal.signal(signal.SIGINT, signal.default_int_handler)
+
+ def _lazy_init(self) -> None:
+ if self._state is _State.CLOSED:
+ raise RuntimeError("Runner is closed")
+ if self._state is _State.INITIALIZED:
+ return
+ if self._loop_factory is None:
+ self._loop = events.new_event_loop()
+ if not self._set_event_loop:
+ # Call set_event_loop only once to avoid calling
+ # attach_loop multiple times on child watchers
+ events.set_event_loop(self._loop)
+ self._set_event_loop = True
+ else:
+ self._loop = self._loop_factory()
+ if self._debug is not None:
+ self._loop.set_debug(self._debug)
+ self._context = contextvars.copy_context()
+ self._state = _State.INITIALIZED
+
+ def _on_sigint(self, signum, frame, main_task: asyncio.Task) -> None:
+ self._interrupt_count += 1
+ if self._interrupt_count == 1 and not main_task.done():
+ main_task.cancel()
+ # wakeup loop if it is blocked by select() with long timeout
+ self._loop.call_soon_threadsafe(lambda: None)
+ return
+ raise KeyboardInterrupt()
+
+ def _cancel_all_tasks(loop: AbstractEventLoop) -> None:
+ to_cancel = tasks.all_tasks(loop)
+ if not to_cancel:
+ return
+
+ for task in to_cancel:
+ task.cancel()
+
+ loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True))
+
+ for task in to_cancel:
+ if task.cancelled():
+ continue
+ if task.exception() is not None:
+ loop.call_exception_handler(
+ {
+ "message": "unhandled exception during asyncio.run() shutdown",
+ "exception": task.exception(),
+ "task": task,
+ }
+ )
+
+ async def _shutdown_default_executor(loop: AbstractEventLoop) -> None:
+ """Schedule the shutdown of the default executor."""
+
+ def _do_shutdown(future: asyncio.futures.Future) -> None:
+ try:
+ loop._default_executor.shutdown(wait=True) # type: ignore[attr-defined]
+ loop.call_soon_threadsafe(future.set_result, None)
+ except Exception as ex:
+ loop.call_soon_threadsafe(future.set_exception, ex)
+
+ loop._executor_shutdown_called = True
+ if loop._default_executor is None:
+ return
+ future = loop.create_future()
+ thread = threading.Thread(target=_do_shutdown, args=(future,))
+ thread.start()
+ try:
+ await future
+ finally:
+ thread.join()
+
+
+T_Retval = TypeVar("T_Retval")
+T_co = TypeVar("T_co", covariant=True)
+T_contra = TypeVar("T_contra", contravariant=True)
+PosArgsT = TypeVarTuple("PosArgsT")
+P = ParamSpec("P")
+
+_root_task: RunVar[asyncio.Task[Any] | None] = RunVar("_root_task")
+
+
+def find_root_task() -> asyncio.Task:
+ root_task = _root_task.get(None)
+ if root_task is not None and not root_task.done():
+ return root_task
+
+ # Look for a task that has been started via run_until_complete()
+ for task in all_tasks():
+ if task._callbacks and not task.done():
+ for cb, context in task._callbacks:
+ if (
+ cb is _run_until_complete_cb
+ or getattr(cb, "__module__", None) == "uvloop.loop"
+ ):
+ _root_task.set(task)
+
+ def _unset(t: asyncio.Task[Any]) -> None:
+ if vars := _run_vars.get(t.get_loop()):
+ vars.pop(_root_task, None)
+
+ # Register a callback to break the task -> loop -> _run_var[loop][_root_task] -> task cycle
+ # Also run it in its own context to not create another reference.
+ # We can't use RunVar.reset() here since these are called synchronously
+ # and thus lowlevel.current_token() (which RunVar.reset() depends on) fails.
+ task.add_done_callback(_unset, context=context)
+ return task
+
+ # Look up the topmost task in the AnyIO task tree, if possible
+ task = cast(asyncio.Task, current_task())
+ state = _task_states.get(task)
+ if state:
+ cancel_scope = state.cancel_scope
+ while cancel_scope and cancel_scope._parent_scope is not None:
+ cancel_scope = cancel_scope._parent_scope
+
+ if cancel_scope is not None:
+ return cast(asyncio.Task, cancel_scope._host_task)
+
+ return task
+
+
+def _task_started(task: asyncio.Task) -> bool:
+ """Return ``True`` if the task has been started and has not finished."""
+ # The task coro should never be None here, as we never add finished tasks to the
+ # task list
+ coro = task.get_coro()
+ assert coro is not None
+ return getcoroutinestate(coro) in (CORO_RUNNING, CORO_SUSPENDED)
+
+
+#
+# Timeouts and cancellation
+#
+
+
+def is_anyio_cancellation(exc: CancelledError) -> bool:
+ # Sometimes third party frameworks catch a CancelledError and raise a new one, so as
+ # a workaround we have to look at the previous ones in __context__ too for a
+ # matching cancel message
+ while True:
+ if (
+ exc.args
+ and isinstance(exc.args[0], str)
+ and exc.args[0].startswith("Cancelled via cancel scope ")
+ ):
+ return True
+
+ if isinstance(exc.__context__, CancelledError):
+ exc = exc.__context__
+ continue
+
+ return False
+
+
+class CancelScope(BaseCancelScope):
+ __slots__ = (
+ "_active",
+ "_cancel_called",
+ "_cancel_handle",
+ "_cancel_reason",
+ "_cancelled_caught",
+ "_child_scopes",
+ "_deadline",
+ "_host_task",
+ "_parent_scope",
+ "_pending_uncancellations",
+ "_shield",
+ "_tasks",
+ "_timeout_handle",
+ )
+
+ def __new__(cls, *, deadline: float = math.inf, shield: bool = False) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self, deadline: float = math.inf, shield: bool = False):
+ self._deadline = deadline
+ self._shield = shield
+ self._parent_scope: CancelScope | None = None
+ self._child_scopes: set[CancelScope] = set()
+ self._cancel_called = False
+ self._cancel_reason: str | None = None
+ self._cancelled_caught = False
+ self._active = False
+ self._timeout_handle: asyncio.TimerHandle | None = None
+ self._cancel_handle: asyncio.Handle | None = None
+ self._tasks: set[asyncio.Task] = set()
+ self._host_task: asyncio.Task | None = None
+ if sys.version_info >= (3, 11):
+ self._pending_uncancellations: int | None = 0
+ else:
+ self._pending_uncancellations = None
+
+ def __enter__(self) -> Self:
+ if self._active:
+ raise RuntimeError(
+ "Each CancelScope may only be used for a single 'with' block"
+ )
+
+ self._host_task = host_task = cast(asyncio.Task, current_task())
+ self._tasks.add(host_task)
+ try:
+ task_state = _task_states[host_task]
+ except KeyError:
+ task_state = TaskState(None, self)
+ _task_states[host_task] = task_state
+ else:
+ self._parent_scope = task_state.cancel_scope
+ task_state.cancel_scope = self
+ if self._parent_scope is not None:
+ # If using an eager task factory, the parent scope may not even contain
+ # the host task
+ self._parent_scope._child_scopes.add(self)
+ self._parent_scope._tasks.discard(host_task)
+
+ self._timeout()
+ self._active = True
+
+ # Start cancelling the host task if the scope was cancelled before entering
+ if self._cancel_called:
+ self._deliver_cancellation(self)
+
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ del exc_tb
+
+ if not self._active:
+ raise RuntimeError("This cancel scope is not active")
+ if current_task() is not self._host_task:
+ raise RuntimeError(
+ "Attempted to exit cancel scope in a different task than it was "
+ "entered in"
+ )
+
+ assert self._host_task is not None
+ host_task_state = _task_states.get(self._host_task)
+ if host_task_state is None or host_task_state.cancel_scope is not self:
+ raise RuntimeError(
+ "Attempted to exit a cancel scope that isn't the current tasks's "
+ "current cancel scope"
+ )
+
+ try:
+ self._active = False
+ if self._timeout_handle:
+ self._timeout_handle.cancel()
+ self._timeout_handle = None
+
+ self._tasks.remove(self._host_task)
+ if self._parent_scope is not None:
+ self._parent_scope._child_scopes.remove(self)
+ self._parent_scope._tasks.add(self._host_task)
+
+ host_task_state.cancel_scope = self._parent_scope
+
+ # Restart the cancellation effort in the closest visible, cancelled parent
+ # scope if necessary
+ self._restart_cancellation_in_parent()
+
+ # We only swallow the exception iff it was an AnyIO CancelledError, either
+ # directly as exc_val or inside an exception group and there are no cancelled
+ # parent cancel scopes visible to us here
+ if self._cancel_called and not self._parent_cancellation_is_visible_to_us:
+ # For each level-cancel() call made on the host task, call uncancel()
+ while self._pending_uncancellations:
+ self._host_task.uncancel()
+ self._pending_uncancellations -= 1
+
+ # Update cancelled_caught and check for exceptions we must not swallow
+ if isinstance(exc_val, BaseExceptionGroup):
+ cancelleds_caught, remaining = exc_val.split(
+ lambda exc: (
+ isinstance(exc, CancelledError)
+ and is_anyio_cancellation(exc)
+ )
+ )
+
+ if cancelleds_caught is None:
+ return False
+
+ self._cancelled_caught = True
+
+ if remaining is None:
+ return True
+
+ context = remaining.__context__
+ try:
+ # Preserve __cause__ and __suppress_context__ by avoiding `raise
+ # ... from ...`
+ raise remaining
+ finally:
+ # Preserve __context__
+ remaining.__context__ = context
+ del context
+ else:
+ if isinstance(exc_val, CancelledError) and is_anyio_cancellation(
+ exc_val
+ ):
+ self._cancelled_caught = True
+ return True
+ else:
+ return False
+ else:
+ if self._pending_uncancellations:
+ assert self._parent_scope is not None
+ assert self._parent_scope._pending_uncancellations is not None
+ self._parent_scope._pending_uncancellations += (
+ self._pending_uncancellations
+ )
+ self._pending_uncancellations = 0
+
+ return False
+ finally:
+ self._host_task = None
+ del exc_val
+
+ @property
+ def _effectively_cancelled(self) -> bool:
+ cancel_scope: CancelScope | None = self
+ while cancel_scope is not None:
+ if cancel_scope._cancel_called:
+ return True
+
+ if cancel_scope.shield:
+ return False
+
+ cancel_scope = cancel_scope._parent_scope
+
+ return False
+
+ @property
+ def _parent_cancellation_is_visible_to_us(self) -> bool:
+ return (
+ self._parent_scope is not None
+ and not self.shield
+ and self._parent_scope._effectively_cancelled
+ )
+
+ def _timeout(self) -> None:
+ if self._deadline != math.inf:
+ loop = get_running_loop()
+ if loop.time() >= self._deadline:
+ self.cancel("deadline exceeded")
+ else:
+ self._timeout_handle = loop.call_at(self._deadline, self._timeout)
+
+ def _deliver_cancellation(self, origin: CancelScope) -> bool:
+ """
+ Deliver cancellation to directly contained tasks and nested cancel scopes.
+
+ Schedule another run at the end if we still have tasks eligible for
+ cancellation.
+
+ :param origin: the cancel scope that originated the cancellation
+ :return: ``True`` if the delivery needs to be retried on the next cycle
+
+ """
+ should_retry = False
+ current = current_task()
+ for task in self._tasks:
+ # Always skip tasks that are already done (see issue #1111)
+ if task.done():
+ continue
+
+ should_retry = True
+ if task._must_cancel: # type: ignore[attr-defined]
+ continue
+
+ # The task is eligible for cancellation if it has started
+ if task is not current and (task is self._host_task or _task_started(task)):
+ waiter = task._fut_waiter # type: ignore[attr-defined]
+ if not isinstance(waiter, asyncio.Future) or not waiter.done():
+ task.cancel(origin._cancel_reason)
+ if (
+ task is origin._host_task
+ and origin._pending_uncancellations is not None
+ ):
+ origin._pending_uncancellations += 1
+
+ # Deliver cancellation to child scopes that aren't shielded or running their own
+ # cancellation callbacks
+ for scope in self._child_scopes:
+ if not scope._shield and not scope.cancel_called:
+ should_retry = scope._deliver_cancellation(origin) or should_retry
+
+ # Schedule another callback if there are still tasks left
+ if origin is self:
+ if should_retry:
+ self._cancel_handle = get_running_loop().call_soon(
+ self._deliver_cancellation, origin
+ )
+ else:
+ self._cancel_handle = None
+
+ return should_retry
+
+ def _restart_cancellation_in_parent(self) -> None:
+ """
+ Restart the cancellation effort in the closest directly cancelled parent scope.
+
+ """
+ scope = self._parent_scope
+ while scope is not None:
+ if scope._cancel_called:
+ if scope._cancel_handle is None:
+ scope._deliver_cancellation(scope)
+
+ break
+
+ # No point in looking beyond any shielded scope
+ if scope._shield:
+ break
+
+ scope = scope._parent_scope
+
+ def _reparent(self, new_parent: CancelScope) -> None:
+ """
+ Move this active scope from its current parent to ``new_parent``.
+
+ Used by :meth:`TaskGroup.start` to move a task that has just called
+ ``task_status.started()`` into the target task group's cancel scope.
+ """
+ if self._parent_scope is new_parent:
+ return
+
+ if self._parent_scope is not None:
+ self._parent_scope._child_scopes.discard(self)
+
+ self._parent_scope = new_parent
+ new_parent._child_scopes.add(self)
+
+ # If the new parent (or an ancestor) is already cancelled, (re)start the
+ # delivery loop to ensure we're cancelled at next checkpoint like Trio.
+ self._restart_cancellation_in_parent()
+
+ def cancel(self, reason: str | None = None) -> None:
+ if not self._cancel_called:
+ if self._timeout_handle:
+ self._timeout_handle.cancel()
+ self._timeout_handle = None
+
+ self._cancel_called = True
+ self._cancel_reason = f"Cancelled via cancel scope {id(self):x}"
+ if task := current_task():
+ self._cancel_reason += f" by {task}"
+
+ if reason:
+ self._cancel_reason += f"; reason: {reason}"
+
+ if self._host_task is not None:
+ self._deliver_cancellation(self)
+
+ @property
+ def deadline(self) -> float:
+ return self._deadline
+
+ @deadline.setter
+ def deadline(self, value: float) -> None:
+ self._deadline = float(value)
+ if self._timeout_handle is not None:
+ self._timeout_handle.cancel()
+ self._timeout_handle = None
+
+ if self._active and not self._cancel_called:
+ self._timeout()
+
+ @property
+ def cancel_called(self) -> bool:
+ return self._cancel_called
+
+ @property
+ def cancelled_caught(self) -> bool:
+ return self._cancelled_caught
+
+ @property
+ def shield(self) -> bool:
+ return self._shield
+
+ @shield.setter
+ def shield(self, value: bool) -> None:
+ if self._shield != value:
+ self._shield = value
+ if not value:
+ self._restart_cancellation_in_parent()
+
+
+#
+# Task states
+#
+
+
+class TaskState:
+ """
+ Encapsulates auxiliary task information that cannot be added to the Task instance
+ itself because there are no guarantees about its implementation.
+ """
+
+ __slots__ = "__weakref__", "cancel_scope", "parent_id"
+
+ def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None):
+ self.parent_id = parent_id
+ self.cancel_scope = cancel_scope
+
+
+_task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary()
+
+
+#
+# Task groups
+#
+
+
+class _AsyncioTaskStatus(abc.TaskStatus[T_contra]):
+ def __init__(
+ self,
+ future: asyncio.Future,
+ parent_id: int,
+ target_scope: CancelScope,
+ spawn_scope: CancelScope,
+ ):
+ self._future = future
+ self._parent_id = parent_id
+ # The eventual parent scope for this spawn_scope
+ # (after task_status.started() has been called)
+ self._target_scope = target_scope
+ # The task's own cancel scope, also held by its TaskHandle
+ self._spawn_scope = spawn_scope
+
+ def started(self, value: T_contra | None = None) -> None:
+ task = cast(asyncio.Task, current_task())
+ _task_states[task].parent_id = self._parent_id
+ if self._future.done():
+ if not self._future.cancelled():
+ raise RuntimeError("called 'started' twice on the same task status")
+ else:
+ # Caller of start() was cancelled, nothing to reparent
+ return
+
+ self._future.set_result(value)
+ self._spawn_scope._reparent(self._target_scope)
+
+
+if sys.version_info >= (3, 12):
+ _eager_task_factory_code: CodeType | None = asyncio.eager_task_factory.__code__
+else:
+ _eager_task_factory_code = None
+
+
+class TaskGroup(abc.TaskGroup):
+ def __init__(self) -> None:
+ self.cancel_scope: CancelScope = CancelScope()
+ self._entered = False
+ self._exceptions: list[BaseException] = []
+ self._tasks: set[asyncio.Task] = set()
+ self._on_completed_fut: asyncio.Future[None] | None = None
+
+ async def __aenter__(self) -> Self:
+ if self._entered:
+ raise RuntimeError("TaskGroup cannot be entered more than once")
+
+ self._entered = True
+
+ self.cancel_scope.__enter__()
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ try:
+ if exc_val is not None:
+ self.cancel_scope.cancel()
+ if not isinstance(exc_val, CancelledError):
+ self._exceptions.append(exc_val)
+
+ loop = get_running_loop()
+ try:
+ if self._tasks:
+ with CancelScope() as wait_scope:
+ while self._tasks:
+ self._on_completed_fut = loop.create_future()
+
+ try:
+ await self._on_completed_fut
+ except CancelledError as exc:
+ # Shield the scope against further cancellation attempts,
+ # as they're not productive (#695)
+ wait_scope.shield = True
+ self.cancel_scope.cancel()
+
+ # Set exc_val from the cancellation exception if it was
+ # previously unset. However, we should not replace a native
+ # cancellation exception with one raise by a cancel scope.
+ if exc_val is None or (
+ isinstance(exc_val, CancelledError)
+ and not is_anyio_cancellation(exc)
+ ):
+ exc_val = exc
+
+ self._on_completed_fut = None
+ else:
+ # If there are no child tasks to wait on, run at least one checkpoint
+ # anyway
+ await AsyncIOBackend.cancel_shielded_checkpoint()
+
+ if self._exceptions:
+ # The exception that got us here should already have been
+ # added to self._exceptions so it's ok to break exception
+ # chaining and avoid adding a "During handling of above..."
+ # for each nesting level.
+ raise BaseExceptionGroup(
+ "unhandled errors in a TaskGroup", self._exceptions
+ ) from None
+ elif exc_val:
+ raise exc_val
+ except BaseException as exc:
+ if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__):
+ return True
+
+ raise
+
+ return self.cancel_scope.__exit__(exc_type, exc_val, exc_tb)
+ finally:
+ del exc_val, exc_tb, self._exceptions
+
+ def _spawn(
+ self,
+ coro: Coroutine[Any, Any, T_co],
+ name: object,
+ task_status: _AsyncioTaskStatus | None = None,
+ ) -> TaskHandle[T_co]:
+ task_status_future = task_status._future if task_status is not None else None
+
+ def task_done(_task: asyncio.Task) -> None:
+ if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None:
+ asyncio.future_discard_from_awaited_by(
+ _task, self.cancel_scope._host_task
+ )
+
+ task_state = _task_states[_task]
+ assert task_state.cancel_scope is not None
+ assert _task in task_state.cancel_scope._tasks
+ task_state.cancel_scope._tasks.remove(_task)
+ self._tasks.remove(task)
+ del _task_states[_task]
+
+ if self._on_completed_fut is not None and not self._tasks:
+ try:
+ self._on_completed_fut.set_result(None)
+ except asyncio.InvalidStateError:
+ pass
+
+ try:
+ exc = _task.exception()
+ except CancelledError as e:
+ while isinstance(e.__context__, CancelledError):
+ e = e.__context__
+
+ exc = e
+
+ if exc is not None:
+ # The future can only be in the cancelled state if the host task was
+ # cancelled, so return immediately instead of adding one more
+ # CancelledError to the exceptions list
+ if task_status_future is not None and task_status_future.cancelled():
+ return
+
+ if task_status_future is None or task_status_future.done():
+ if not isinstance(exc, CancelledError):
+ self._exceptions.append(exc)
+
+ if not self.cancel_scope._effectively_cancelled:
+ self.cancel_scope.cancel()
+ else:
+ task_status_future.set_exception(exc)
+ elif task_status_future is not None and not task_status_future.done():
+ task_status_future.set_exception(
+ RuntimeError("Child exited without calling task_status.started()")
+ )
+
+ if task_status_future is not None:
+ parent_id = id(current_task())
+ caller_state = _task_states.get(cast(asyncio.Task, current_task()))
+ if caller_state is not None and caller_state.cancel_scope is not None:
+ initial_scope = caller_state.cancel_scope
+ else:
+ # The caller is an unmanaged task (no task state)
+ initial_scope = self.cancel_scope
+ else:
+ parent_id = id(self.cancel_scope._host_task)
+ initial_scope = self.cancel_scope
+
+ spawn_scope = task_status._spawn_scope if task_status is not None else None
+ handle = TaskHandle(coro, name, cancel_scope=spawn_scope)
+ loop = asyncio.get_running_loop()
+ wrapper_coro = handle._run_coro()
+ try:
+ if (
+ (factory := loop.get_task_factory())
+ and getattr(factory, "__code__", None) is _eager_task_factory_code
+ and (closure := getattr(factory, "__closure__", None))
+ ):
+ custom_task_constructor = closure[0].cell_contents
+ task = custom_task_constructor(
+ wrapper_coro, loop=loop, name=handle.name
+ )
+ else:
+ task = loop.create_task(wrapper_coro, name=handle.name)
+ except BaseException:
+ with suppress(BaseException):
+ wrapper_coro.close()
+
+ with suppress(BaseException):
+ coro.close()
+
+ raise
+
+ # Make the spawned task inherit the initial cancel scope
+ _task_states[task] = TaskState(parent_id=parent_id, cancel_scope=initial_scope)
+ initial_scope._tasks.add(task)
+ self._tasks.add(task)
+ if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None:
+ asyncio.future_add_to_awaited_by(task, self.cancel_scope._host_task)
+
+ task.add_done_callback(task_done)
+ return handle
+
+ def create_task(
+ self,
+ coro: Coroutine[Any, Any, T_co],
+ *,
+ name: object = None,
+ context: Context | None = None,
+ ) -> TaskHandle[T_co]:
+ if not isinstance(coro, Coroutine):
+ raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}")
+
+ if not self._entered or not self.cancel_scope._active:
+ coro.close()
+ raise RuntimeError(
+ "This task group is not active; no new tasks can be started."
+ )
+
+ final_name = get_coro_name(coro, name)
+
+ if context is not None:
+ return context.run(self._spawn, coro, name=final_name)
+ else:
+ return self._spawn(coro, name=final_name)
+
+ async def start(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ return_handle: Literal[False, True] = False,
+ ) -> Any:
+ if not self._entered or not self.cancel_scope._active:
+ raise RuntimeError(
+ "This task group is not active; no new tasks can be started."
+ )
+
+ # Until task_status.started() is called, a task spawned via start() belongs to
+ # the *caller's* cancel scope, not the target group's, so cancelling the group
+ # does not cancel a task that hasn't reported startup yet. The
+ # task_status.started() call moves the task's cancel scope to this task group's
+ # cancel scope.
+ #
+ # The caller may be an unmanaged task (no _task_states entry), in which case
+ # fall back to the group's own scope.
+ future: asyncio.Future = asyncio.Future()
+ final_name = get_callable_name(func, name)
+ task_status: _AsyncioTaskStatus[Any] = _AsyncioTaskStatus(
+ future, id(self.cancel_scope._host_task), self.cancel_scope, CancelScope()
+ )
+ coro = call_for_coroutine(func, args, task_status=task_status)
+ handle = self._spawn(coro, final_name, task_status)
+
+ # If the task raises an exception after sending a start value without a switch
+ # point between, the task group is cancelled and this method never proceeds to
+ # process the completed future. That's why we have to have a shielded cancel
+ # scope here.
+ try:
+ await future
+ except BaseException:
+ if handle.status is TaskHandle.Status.PENDING:
+ # Cancel the task and wait for it to exit before returning
+ handle.cancel()
+ with CancelScope(shield=True):
+ await handle.wait()
+
+ raise
+
+ if return_handle:
+ handle._start_value = future.result()
+ return handle
+ else:
+ return future.result()
+
+
+#
+# Threads
+#
+
+_Retval_Queue_Type = tuple[T_Retval | None, BaseException | None]
+
+
+class WorkerThread(Thread):
+ MAX_IDLE_TIME = 10 # seconds
+
+ def __init__(
+ self,
+ root_task: asyncio.Task,
+ workers: set[WorkerThread],
+ idle_workers: deque[WorkerThread],
+ ):
+ kwargs: dict[str, Any] = {}
+ if sys.version_info >= (3, 14):
+ kwargs["context"] = Context()
+
+ super().__init__(name="AnyIO worker thread", **kwargs)
+ self.root_task = root_task
+ self.workers = workers
+ self.idle_workers = idle_workers
+ self.loop = root_task._loop
+ self.queue: Queue[
+ tuple[Context, Callable, tuple, asyncio.Future, CancelScope] | None
+ ] = Queue(2)
+ self.idle_since = AsyncIOBackend.current_time()
+ self.stopping = False
+
+ def _report_result(
+ self, future: asyncio.Future, result: Any, exc: BaseException | None
+ ) -> None:
+ self.idle_since = AsyncIOBackend.current_time()
+ if not self.stopping:
+ self.idle_workers.append(self)
+
+ if not future.cancelled():
+ if exc is not None:
+ if isinstance(exc, StopIteration):
+ new_exc = RuntimeError("coroutine raised StopIteration")
+ new_exc.__cause__ = exc
+ exc = new_exc
+
+ future.set_exception(exc)
+ else:
+ future.set_result(result)
+
+ def run(self) -> None:
+ with claim_worker_thread(AsyncIOBackend, self.loop):
+ while True:
+ item = self.queue.get()
+ if item is None:
+ # Shutdown command received
+ return
+
+ context, func, args, future, cancel_scope = item
+ if not future.cancelled():
+ result = None
+ exception: BaseException | None = None
+ threadlocals.current_cancel_scope = cancel_scope
+ try:
+ result = context.run(func, *args)
+ except BaseException as exc:
+ exception = exc
+ finally:
+ del threadlocals.current_cancel_scope
+
+ try:
+ self.loop.call_soon_threadsafe(
+ self._report_result, future, result, exception
+ )
+ except RuntimeError:
+ if not self.loop.is_closed():
+ raise
+
+ del result, exception
+
+ self.queue.task_done()
+ del item, context, func, args, future, cancel_scope
+
+ def stop(self, f: asyncio.Task | None = None) -> None:
+ self.stopping = True
+ self.queue.put_nowait(None)
+ self.workers.discard(self)
+ try:
+ self.idle_workers.remove(self)
+ except ValueError:
+ pass
+
+
+_threadpool_idle_workers: RunVar[deque[WorkerThread]] = RunVar(
+ "_threadpool_idle_workers"
+)
+_threadpool_workers: RunVar[set[WorkerThread]] = RunVar("_threadpool_workers")
+
+
+#
+# Subprocesses
+#
+
+
+@dataclass(eq=False)
+class StreamReaderWrapper(abc.ByteReceiveStream):
+ _stream: asyncio.StreamReader
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ data = await self._stream.read(max_bytes)
+ if data:
+ return data
+ else:
+ raise EndOfStream
+
+ async def aclose(self) -> None:
+ self._stream.set_exception(ClosedResourceError())
+ await AsyncIOBackend.checkpoint()
+
+
+@dataclass(eq=False)
+class StreamWriterWrapper(abc.ByteSendStream):
+ _stream: asyncio.StreamWriter
+ _closed: bool = field(init=False, default=False)
+
+ async def send(self, item: bytes) -> None:
+ await AsyncIOBackend.checkpoint_if_cancelled()
+ stream_paused = self._stream._protocol._paused # type: ignore[attr-defined]
+ try:
+ self._stream.write(item)
+ await self._stream.drain()
+ except (ConnectionResetError, BrokenPipeError, RuntimeError) as exc:
+ # If closed by us and/or the peer:
+ # * on stdlib, drain() raises ConnectionResetError or BrokenPipeError
+ # * on uvloop and Winloop, write() eventually starts raising RuntimeError
+ if self._closed:
+ raise ClosedResourceError from exc
+ elif self._stream.is_closing():
+ raise BrokenResourceError from exc
+
+ raise
+
+ if not stream_paused:
+ await AsyncIOBackend.cancel_shielded_checkpoint()
+
+ async def aclose(self) -> None:
+ self._closed = True
+ self._stream.close()
+ await AsyncIOBackend.checkpoint()
+
+
+@dataclass(eq=False)
+class Process(abc.Process):
+ _process: asyncio.subprocess.Process
+ _stdin: StreamWriterWrapper | None
+ _stdout: StreamReaderWrapper | None
+ _stderr: StreamReaderWrapper | None
+ _exited: asyncio.Event
+ _transport: asyncio.SubprocessTransport
+
+ async def aclose(self) -> None:
+ with CancelScope(shield=True) as scope:
+ # We need to close the underlying pipe_transports as well to allow a
+ # process blocking on full buffers to receive SIGPIPE and exit.
+ if self._stdin:
+ await self._stdin.aclose()
+ if pipe := self._transport.get_pipe_transport(0):
+ pipe.close()
+ if self._stdout:
+ await self._stdout.aclose()
+ if pipe := self._transport.get_pipe_transport(1):
+ pipe.close()
+ if self._stderr:
+ await self._stderr.aclose()
+ if pipe := self._transport.get_pipe_transport(2):
+ pipe.close()
+
+ scope.shield = False
+ try:
+ await self.wait()
+ except BaseException:
+ scope.shield = True
+ # Closing the transport on asyncio also handles sending kill
+ self._transport.close()
+ await self.wait()
+ raise
+
+ async def wait(self) -> int:
+ await self._exited.wait()
+ assert self._process.returncode is not None
+ return self._process.returncode
+
+ def terminate(self) -> None:
+ self._process.terminate()
+
+ def kill(self) -> None:
+ self._process.kill()
+
+ def send_signal(self, signal: int) -> None:
+ self._process.send_signal(signal)
+
+ @property
+ def pid(self) -> int:
+ return self._process.pid
+
+ @property
+ def returncode(self) -> int | None:
+ return self._process.returncode
+
+ @property
+ def stdin(self) -> abc.ByteSendStream | None:
+ return self._stdin
+
+ @property
+ def stdout(self) -> abc.ByteReceiveStream | None:
+ return self._stdout
+
+ @property
+ def stderr(self) -> abc.ByteReceiveStream | None:
+ return self._stderr
+
+
+def _forcibly_shutdown_process_pool_on_exit(
+ workers: set[Process], _task: object
+) -> None:
+ """
+ Forcibly shuts down worker processes belonging to this event loop."""
+ child_watcher: asyncio.AbstractChildWatcher | None = None # type: ignore[name-defined]
+ if sys.version_info < (3, 12):
+ try:
+ child_watcher = asyncio.get_event_loop_policy().get_child_watcher()
+ except NotImplementedError:
+ pass
+
+ # Close as much as possible (w/o async/await) to avoid warnings
+ for process in workers.copy():
+ if process.returncode is not None:
+ continue
+
+ process._stdin._stream._transport.close() # type: ignore[union-attr]
+ process._stdout._stream._transport.close() # type: ignore[union-attr]
+ process._stderr._stream._transport.close() # type: ignore[union-attr]
+ process.kill()
+ if child_watcher:
+ child_watcher.remove_child_handler(process.pid)
+
+
+async def _shutdown_process_pool_on_exit(workers: set[abc.Process]) -> None:
+ """
+ Shuts down worker processes belonging to this event loop.
+
+ NOTE: this only works when the event loop was started using asyncio.run() or
+ anyio.run().
+
+ """
+ process: abc.Process
+ try:
+ await sleep(math.inf)
+ except asyncio.CancelledError:
+ workers = workers.copy()
+ for process in workers:
+ if process.returncode is None:
+ process.kill()
+
+ for process in workers:
+ await process.aclose()
+
+
+#
+# Sockets and networking
+#
+
+
+class StreamProtocol(asyncio.Protocol):
+ read_queue: deque[bytes]
+ read_event: asyncio.Event
+ write_event: asyncio.Event
+ exception: Exception | None = None
+ is_at_eof: bool = False
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ self.read_queue = deque()
+ self.read_event = asyncio.Event()
+ self.write_event = asyncio.Event()
+ self.write_event.set()
+ cast(asyncio.Transport, transport).set_write_buffer_limits(0)
+
+ def connection_lost(self, exc: Exception | None) -> None:
+ if exc:
+ self.exception = exc
+
+ self.read_event.set()
+ self.write_event.set()
+
+ def data_received(self, data: bytes) -> None:
+ # ProactorEventloop sometimes sends bytearray instead of bytes
+ self.read_queue.append(bytes(data))
+ self.read_event.set()
+
+ def eof_received(self) -> bool | None:
+ self.is_at_eof = True
+ self.read_event.set()
+ return True
+
+ def pause_writing(self) -> None:
+ self.write_event = asyncio.Event()
+
+ def resume_writing(self) -> None:
+ self.write_event.set()
+
+
+class DatagramProtocol(asyncio.DatagramProtocol):
+ read_queue: deque[tuple[bytes, IPSockAddrType]]
+ read_event: asyncio.Event
+ write_event: asyncio.Event
+ closed_event: asyncio.Event
+ exception: Exception | None = None
+
+ def connection_made(self, transport: asyncio.BaseTransport) -> None:
+ self.read_queue = deque(maxlen=100) # arbitrary value
+ self.read_event = asyncio.Event()
+ self.write_event = asyncio.Event()
+ self.closed_event = asyncio.Event()
+ self.write_event.set()
+
+ def connection_lost(self, exc: Exception | None) -> None:
+ self.read_event.set()
+ self.write_event.set()
+ self.closed_event.set()
+
+ def datagram_received(self, data: bytes, addr: IPSockAddrType) -> None:
+ addr = convert_ipv6_sockaddr(addr)
+ self.read_queue.append((data, addr))
+ self.read_event.set()
+
+ def error_received(self, exc: Exception) -> None:
+ self.exception = exc
+
+ def pause_writing(self) -> None:
+ self.write_event.clear()
+
+ def resume_writing(self) -> None:
+ self.write_event.set()
+
+
+class SocketStream(abc.SocketStream):
+ def __init__(self, transport: asyncio.Transport, protocol: StreamProtocol):
+ self._transport = transport
+ self._protocol = protocol
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+ self._closed = False
+
+ @property
+ def _raw_socket(self) -> socket.socket:
+ return self._transport.get_extra_info("socket")
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ with self._receive_guard:
+ if (
+ not self._protocol.read_event.is_set()
+ and not self._transport.is_closing()
+ and not self._protocol.is_at_eof
+ ):
+ self._transport.resume_reading()
+ await self._protocol.read_event.wait()
+ self._transport.pause_reading()
+ else:
+ await AsyncIOBackend.checkpoint()
+
+ try:
+ chunk = self._protocol.read_queue.popleft()
+ except IndexError:
+ if self._closed:
+ raise ClosedResourceError from None
+ elif self._protocol.exception:
+ raise BrokenResourceError from self._protocol.exception
+ else:
+ raise EndOfStream from None
+
+ if len(chunk) > max_bytes:
+ # Split the oversized chunk
+ chunk, leftover = chunk[:max_bytes], chunk[max_bytes:]
+ self._protocol.read_queue.appendleft(leftover)
+
+ # If the read queue is empty, clear the flag so that the next call will
+ # block until data is available
+ if not self._protocol.read_queue:
+ self._protocol.read_event.clear()
+
+ return chunk
+
+ async def send(self, item: bytes) -> None:
+ with self._send_guard:
+ await AsyncIOBackend.checkpoint()
+
+ if self._closed:
+ raise ClosedResourceError
+ elif self._protocol.exception is not None:
+ raise BrokenResourceError from self._protocol.exception
+
+ try:
+ self._transport.write(item)
+ except RuntimeError as exc:
+ if self._transport.is_closing():
+ raise BrokenResourceError from exc
+ else:
+ raise
+
+ await self._protocol.write_event.wait()
+
+ async def send_eof(self) -> None:
+ try:
+ self._transport.write_eof()
+ except OSError:
+ pass
+
+ async def aclose(self) -> None:
+ self._closed = True
+ if not self._transport.is_closing():
+ try:
+ self._transport.write_eof()
+ except OSError:
+ pass
+
+ self._transport.close()
+ await sleep(0)
+ self._transport.abort()
+
+
+class _RawSocketMixin:
+ _receive_future: asyncio.Future | None = None
+ _send_future: asyncio.Future | None = None
+ _closing = False
+
+ def __init__(self, raw_socket: socket.socket):
+ self.__raw_socket = raw_socket
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+
+ @property
+ def _raw_socket(self) -> socket.socket:
+ return self.__raw_socket
+
+ def _wait_until_readable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future:
+ def callback(f: object) -> None:
+ del self._receive_future
+ loop.remove_reader(self.__raw_socket)
+
+ f = self._receive_future = asyncio.Future()
+ loop.add_reader(self.__raw_socket, f.set_result, None)
+ f.add_done_callback(callback)
+ return f
+
+ def _wait_until_writable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future:
+ def callback(f: object) -> None:
+ del self._send_future
+ loop.remove_writer(self.__raw_socket)
+
+ f = self._send_future = asyncio.Future()
+ loop.add_writer(self.__raw_socket, f.set_result, None)
+ f.add_done_callback(callback)
+ return f
+
+ async def aclose(self) -> None:
+ if not self._closing:
+ self._closing = True
+ if self.__raw_socket.fileno() != -1:
+ self.__raw_socket.close()
+
+ if self._receive_future and not self._receive_future.done():
+ self._receive_future.set_result(None)
+ if self._send_future and not self._send_future.done():
+ self._send_future.set_result(None)
+
+
+class UNIXSocketStream(_RawSocketMixin, abc.UNIXSocketStream):
+ async def send_eof(self) -> None:
+ with self._send_guard:
+ self._raw_socket.shutdown(socket.SHUT_WR)
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ loop = get_running_loop()
+ await AsyncIOBackend.checkpoint()
+ with self._receive_guard:
+ while True:
+ try:
+ data = self._raw_socket.recv(max_bytes)
+ except BlockingIOError:
+ await self._wait_until_readable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+ else:
+ if not data:
+ raise EndOfStream
+
+ return data
+
+ async def send(self, item: bytes) -> None:
+ loop = get_running_loop()
+ await AsyncIOBackend.checkpoint()
+ with self._send_guard:
+ view = memoryview(item)
+ while view:
+ try:
+ bytes_sent = self._raw_socket.send(view)
+ except BlockingIOError:
+ await self._wait_until_writable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+ else:
+ view = view[bytes_sent:]
+
+ async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]:
+ if not isinstance(msglen, int) or msglen < 0:
+ raise ValueError("msglen must be a non-negative integer")
+ if not isinstance(maxfds, int) or maxfds < 1:
+ raise ValueError("maxfds must be a positive integer")
+
+ loop = get_running_loop()
+ fds = array.array("i")
+ await AsyncIOBackend.checkpoint()
+ with self._receive_guard:
+ while True:
+ try:
+ message, ancdata, _flags, _addr = self._raw_socket.recvmsg(
+ msglen, socket.CMSG_LEN(maxfds * fds.itemsize)
+ )
+ except BlockingIOError:
+ await self._wait_until_readable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+ else:
+ if not message and not ancdata:
+ raise EndOfStream
+
+ break
+
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
+ if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS:
+ raise RuntimeError(
+ f"Received unexpected ancillary data; message = {message!r}, "
+ f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}"
+ )
+
+ fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
+
+ return message, list(fds)
+
+ async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None:
+ if not message:
+ raise ValueError("message must not be empty")
+ if not fds:
+ raise ValueError("fds must not be empty")
+
+ loop = get_running_loop()
+ filenos: list[int] = []
+ for fd in fds:
+ if isinstance(fd, int):
+ filenos.append(fd)
+ elif isinstance(fd, IOBase):
+ filenos.append(fd.fileno())
+
+ fdarray = array.array("i", filenos)
+ await AsyncIOBackend.checkpoint()
+ with self._send_guard:
+ while True:
+ try:
+ # The ignore can be removed after mypy picks up
+ # https://github.com/python/typeshed/pull/5545
+ self._raw_socket.sendmsg(
+ [message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray)]
+ )
+ break
+ except BlockingIOError:
+ await self._wait_until_writable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+
+
+class TCPSocketListener(abc.SocketListener):
+ _accept_scope: CancelScope | None = None
+ _closed = False
+
+ def __init__(self, raw_socket: socket.socket):
+ self.__raw_socket = raw_socket
+ self._loop = cast(asyncio.BaseEventLoop, get_running_loop())
+ self._accept_guard = ResourceGuard("accepting connections from")
+
+ @property
+ def _raw_socket(self) -> socket.socket:
+ return self.__raw_socket
+
+ async def accept(self) -> abc.SocketStream:
+ if self._closed:
+ raise ClosedResourceError
+
+ with self._accept_guard:
+ await AsyncIOBackend.checkpoint()
+ with CancelScope() as self._accept_scope:
+ try:
+ client_sock, _addr = await self._loop.sock_accept(self._raw_socket)
+ except asyncio.CancelledError:
+ # Workaround for https://bugs.python.org/issue41317
+ try:
+ self._loop.remove_reader(self._raw_socket)
+ except (ValueError, NotImplementedError):
+ pass
+
+ if self._closed:
+ raise ClosedResourceError from None
+
+ raise
+ finally:
+ self._accept_scope = None
+
+ client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ transport, protocol = await self._loop.connect_accepted_socket(
+ StreamProtocol, client_sock
+ )
+ return SocketStream(transport, protocol)
+
+ async def aclose(self) -> None:
+ if self._closed:
+ return
+
+ self._closed = True
+ if self._accept_scope:
+ # Workaround for https://bugs.python.org/issue41317
+ try:
+ self._loop.remove_reader(self._raw_socket)
+ except (ValueError, NotImplementedError):
+ pass
+
+ self._accept_scope.cancel()
+ await sleep(0)
+
+ self._raw_socket.close()
+
+
+class UNIXSocketListener(abc.SocketListener):
+ def __init__(self, raw_socket: socket.socket):
+ self.__raw_socket = raw_socket
+ self._loop = get_running_loop()
+ self._accept_guard = ResourceGuard("accepting connections from")
+ self._closed = False
+
+ async def accept(self) -> abc.SocketStream:
+ await AsyncIOBackend.checkpoint()
+ with self._accept_guard:
+ while True:
+ try:
+ client_sock, _ = self.__raw_socket.accept()
+ client_sock.setblocking(False)
+ return UNIXSocketStream(client_sock)
+ except BlockingIOError:
+ f: asyncio.Future = asyncio.Future()
+ self._loop.add_reader(self.__raw_socket, f.set_result, None)
+ f.add_done_callback(
+ lambda _: self._loop.remove_reader(self.__raw_socket)
+ )
+ await f
+ except OSError as exc:
+ if self._closed:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+
+ async def aclose(self) -> None:
+ self._closed = True
+ self.__raw_socket.close()
+
+ @property
+ def _raw_socket(self) -> socket.socket:
+ return self.__raw_socket
+
+
+class UDPSocket(abc.UDPSocket):
+ def __init__(
+ self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol
+ ):
+ self._transport = transport
+ self._protocol = protocol
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+ self._closed = False
+
+ @property
+ def _raw_socket(self) -> socket.socket:
+ return self._transport.get_extra_info("socket")
+
+ async def aclose(self) -> None:
+ self._closed = True
+ if not self._transport.is_closing():
+ self._transport.close()
+
+ await self._protocol.closed_event.wait()
+
+ async def receive(self) -> tuple[bytes, IPSockAddrType]:
+ with self._receive_guard:
+ await AsyncIOBackend.checkpoint()
+
+ # If the buffer is empty, ask for more data
+ if not self._protocol.read_queue and not self._transport.is_closing():
+ self._protocol.read_event.clear()
+ await self._protocol.read_event.wait()
+
+ try:
+ return self._protocol.read_queue.popleft()
+ except IndexError:
+ if self._closed:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from None
+
+ async def send(self, item: UDPPacketType) -> None:
+ with self._send_guard:
+ await AsyncIOBackend.checkpoint()
+ await self._protocol.write_event.wait()
+ if self._closed:
+ raise ClosedResourceError
+ elif self._transport.is_closing():
+ raise BrokenResourceError
+ else:
+ self._transport.sendto(*item)
+
+
+class ConnectedUDPSocket(abc.ConnectedUDPSocket):
+ def __init__(
+ self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol
+ ):
+ self._transport = transport
+ self._protocol = protocol
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+ self._closed = False
+
+ @property
+ def _raw_socket(self) -> socket.socket:
+ return self._transport.get_extra_info("socket")
+
+ async def aclose(self) -> None:
+ self._closed = True
+ if not self._transport.is_closing():
+ self._transport.close()
+
+ await self._protocol.closed_event.wait()
+
+ async def receive(self) -> bytes:
+ with self._receive_guard:
+ await AsyncIOBackend.checkpoint()
+
+ # If the buffer is empty, ask for more data
+ if not self._protocol.read_queue and not self._transport.is_closing():
+ self._protocol.read_event.clear()
+ await self._protocol.read_event.wait()
+
+ try:
+ packet = self._protocol.read_queue.popleft()
+ except IndexError:
+ if self._closed:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from None
+
+ return packet[0]
+
+ async def send(self, item: bytes) -> None:
+ with self._send_guard:
+ await AsyncIOBackend.checkpoint()
+ await self._protocol.write_event.wait()
+ if self._closed:
+ raise ClosedResourceError
+ elif self._transport.is_closing():
+ raise BrokenResourceError
+ else:
+ self._transport.sendto(item)
+
+
+class UNIXDatagramSocket(_RawSocketMixin, abc.UNIXDatagramSocket):
+ async def receive(self) -> UNIXDatagramPacketType:
+ loop = get_running_loop()
+ await AsyncIOBackend.checkpoint()
+ with self._receive_guard:
+ while True:
+ try:
+ data = self._raw_socket.recvfrom(65536)
+ except BlockingIOError:
+ await self._wait_until_readable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+ else:
+ return data
+
+ async def send(self, item: UNIXDatagramPacketType) -> None:
+ loop = get_running_loop()
+ await AsyncIOBackend.checkpoint()
+ with self._send_guard:
+ while True:
+ try:
+ self._raw_socket.sendto(*item)
+ except BlockingIOError:
+ await self._wait_until_writable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+ else:
+ return
+
+
+class ConnectedUNIXDatagramSocket(_RawSocketMixin, abc.ConnectedUNIXDatagramSocket):
+ async def receive(self) -> bytes:
+ loop = get_running_loop()
+ await AsyncIOBackend.checkpoint()
+ with self._receive_guard:
+ while True:
+ try:
+ data = self._raw_socket.recv(65536)
+ except BlockingIOError:
+ await self._wait_until_readable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+ else:
+ return data
+
+ async def send(self, item: bytes) -> None:
+ loop = get_running_loop()
+ await AsyncIOBackend.checkpoint()
+ with self._send_guard:
+ while True:
+ try:
+ self._raw_socket.send(item)
+ except BlockingIOError:
+ await self._wait_until_writable(loop)
+ except OSError as exc:
+ if self._closing:
+ raise ClosedResourceError from None
+ else:
+ raise BrokenResourceError from exc
+ else:
+ return
+
+
+_read_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("read_events")
+_write_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("write_events")
+
+
+#
+# Synchronization
+#
+
+
+class Event(BaseEvent):
+ __slots__ = ("_event",)
+
+ def __new__(cls) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self) -> None:
+ self._event = asyncio.Event()
+
+ def set(self) -> None:
+ self._event.set()
+
+ def is_set(self) -> bool:
+ return self._event.is_set()
+
+ async def wait(self) -> None:
+ if self.is_set():
+ await AsyncIOBackend.checkpoint()
+ else:
+ await self._event.wait()
+
+ def statistics(self) -> EventStatistics:
+ return EventStatistics(len(self._event._waiters))
+
+
+class Lock(BaseLock):
+ __slots__ = "_fast_acquire", "_owner_task", "_waiters"
+
+ def __new__(cls, *, fast_acquire: bool = False) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self, *, fast_acquire: bool = False) -> None:
+ self._fast_acquire = fast_acquire
+ self._owner_task: asyncio.Task | None = None
+ self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque()
+
+ async def acquire(self) -> None:
+ task = cast(asyncio.Task, current_task())
+ if self._owner_task is None and not self._waiters:
+ await AsyncIOBackend.checkpoint_if_cancelled()
+ self._owner_task = task
+
+ # Unless on the "fast path", yield control of the event loop so that other
+ # tasks can run too
+ if not self._fast_acquire:
+ try:
+ await AsyncIOBackend.cancel_shielded_checkpoint()
+ except CancelledError:
+ self.release()
+ raise
+
+ return
+
+ if self._owner_task == task:
+ raise RuntimeError("Attempted to acquire an already held Lock")
+
+ fut: asyncio.Future[None] = asyncio.Future()
+ item = task, fut
+ self._waiters.append(item)
+ try:
+ await fut
+ except CancelledError:
+ if fut.cancelled():
+ try:
+ self._waiters.remove(item)
+ except ValueError:
+ pass
+ else:
+ self.release()
+
+ raise
+
+ def acquire_nowait(self) -> None:
+ task = cast(asyncio.Task, current_task())
+ if self._owner_task is None and not self._waiters:
+ self._owner_task = task
+ return
+
+ if self._owner_task is task:
+ raise RuntimeError("Attempted to acquire an already held Lock")
+
+ raise WouldBlock
+
+ def locked(self) -> bool:
+ return self._owner_task is not None
+
+ def release(self) -> None:
+ if self._owner_task != current_task():
+ raise RuntimeError("The current task is not holding this lock")
+
+ # A cancelled waiter that already received ownership removes itself from
+ # _waiters before calling release(); any cancelled waiter still queued here
+ # was cancelled before being woken, so drop it.
+ while self._waiters:
+ task, fut = self._waiters.popleft()
+ if fut.cancelled():
+ continue
+
+ self._owner_task = task
+ fut.set_result(None)
+ return
+
+ self._owner_task = None
+
+ def statistics(self) -> LockStatistics:
+ task_info = AsyncIOTaskInfo(self._owner_task) if self._owner_task else None
+ return LockStatistics(self.locked(), task_info, len(self._waiters))
+
+
+class Semaphore(BaseSemaphore):
+ __slots__ = "_fast_acquire", "_max_value", "_value", "_waiters"
+
+ def __new__(
+ cls,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> Self:
+ return object.__new__(cls)
+
+ def __init__(
+ self,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ):
+ super().__init__(initial_value, max_value=max_value)
+ self._value = initial_value
+ self._max_value = max_value
+ self._fast_acquire = fast_acquire
+ self._waiters: deque[asyncio.Future[None]] = deque()
+
+ async def acquire(self) -> None:
+ if self._value > 0 and not self._waiters:
+ await AsyncIOBackend.checkpoint_if_cancelled()
+ self._value -= 1
+
+ # Unless on the "fast path", yield control of the event loop so that other
+ # tasks can run too
+ if not self._fast_acquire:
+ try:
+ await AsyncIOBackend.cancel_shielded_checkpoint()
+ except CancelledError:
+ self.release()
+ raise
+
+ return
+
+ fut: asyncio.Future[None] = asyncio.Future()
+ self._waiters.append(fut)
+ try:
+ await fut
+ except CancelledError:
+ if fut.cancelled():
+ try:
+ self._waiters.remove(fut)
+ except ValueError:
+ pass
+ else:
+ self.release()
+
+ raise
+
+ def acquire_nowait(self) -> None:
+ if self._value == 0:
+ raise WouldBlock
+
+ self._value -= 1
+
+ def release(self) -> None:
+ if self._max_value is not None and self._value == self._max_value:
+ raise ValueError("semaphore released too many times")
+
+ while self._waiters:
+ fut = self._waiters.popleft()
+ if fut.cancelled():
+ continue
+
+ fut.set_result(None)
+ return
+
+ self._value += 1
+
+ @property
+ def value(self) -> int:
+ return self._value
+
+ @property
+ def max_value(self) -> int | None:
+ return self._max_value
+
+ def statistics(self) -> SemaphoreStatistics:
+ return SemaphoreStatistics(len(self._waiters))
+
+
+class CapacityLimiter(BaseCapacityLimiter):
+ __slots__ = "_borrowers", "_total_tokens", "_wait_queue"
+
+ def __new__(cls, total_tokens: float) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self, total_tokens: float):
+ self._total_tokens: float = 0
+ self._borrowers: set[Any] = set()
+ self._wait_queue: OrderedDict[Any, asyncio.Event] = OrderedDict()
+ self.total_tokens = total_tokens
+
+ async def __aenter__(self) -> None:
+ await self.acquire()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.release()
+
+ @property
+ def total_tokens(self) -> float:
+ return self._total_tokens
+
+ @total_tokens.setter
+ def total_tokens(self, value: float) -> None:
+ if not isinstance(value, int) and not math.isinf(value):
+ raise TypeError("total_tokens must be an int or math.inf")
+
+ if value < 0:
+ raise ValueError("total_tokens must be >= 0")
+
+ self._total_tokens = value
+
+ # Notify waiting tasks that they have acquired the limiter while
+ # there is spare capacity.
+ while self._wait_queue and len(self._borrowers) < self._total_tokens:
+ borrower, event = self._wait_queue.popitem(last=False)
+ self._borrowers.add(borrower)
+ event.set()
+
+ @property
+ def borrowed_tokens(self) -> int:
+ return len(self._borrowers)
+
+ @property
+ def available_tokens(self) -> float:
+ return self._total_tokens - len(self._borrowers)
+
+ def _notify_next_waiter(self) -> None:
+ """Hand a free token to the next task in line, if any."""
+ if self._wait_queue and len(self._borrowers) < self._total_tokens:
+ borrower, event = self._wait_queue.popitem(last=False)
+ self._borrowers.add(borrower)
+ event.set()
+
+ def acquire_nowait(self) -> None:
+ self.acquire_on_behalf_of_nowait(current_task())
+
+ def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
+ if borrower in self._borrowers:
+ raise RuntimeError(
+ "this borrower is already holding one of this CapacityLimiter's tokens"
+ )
+
+ if self._wait_queue or len(self._borrowers) >= self._total_tokens:
+ raise WouldBlock
+
+ self._borrowers.add(borrower)
+
+ async def acquire(self) -> None:
+ return await self.acquire_on_behalf_of(current_task())
+
+ async def acquire_on_behalf_of(self, borrower: object) -> None:
+ await AsyncIOBackend.checkpoint_if_cancelled()
+ try:
+ self.acquire_on_behalf_of_nowait(borrower)
+ except WouldBlock:
+ event = asyncio.Event()
+ self._wait_queue[borrower] = event
+ try:
+ await event.wait()
+ except BaseException:
+ self._wait_queue.pop(borrower, None)
+ if event.is_set():
+ self._borrowers.discard(borrower)
+ self._notify_next_waiter()
+
+ raise
+ else:
+ try:
+ await AsyncIOBackend.cancel_shielded_checkpoint()
+ except BaseException:
+ self.release()
+ raise
+
+ def release(self) -> None:
+ self.release_on_behalf_of(current_task())
+
+ def release_on_behalf_of(self, borrower: object) -> None:
+ try:
+ self._borrowers.remove(borrower)
+ except KeyError:
+ raise RuntimeError(
+ "this borrower isn't holding any of this CapacityLimiter's tokens"
+ ) from None
+
+ self._notify_next_waiter()
+
+ def statistics(self) -> CapacityLimiterStatistics:
+ return CapacityLimiterStatistics(
+ self.borrowed_tokens,
+ self.total_tokens,
+ tuple(self._borrowers),
+ len(self._wait_queue),
+ )
+
+
+_default_thread_limiter: RunVar[CapacityLimiter] = RunVar("_default_thread_limiter")
+
+
+#
+# Operating system signals
+#
+
+
+class _SignalReceiver:
+ def __init__(self, signals: tuple[Signals, ...]):
+ self._signals = signals
+ self._loop = get_running_loop()
+ self._signal_queue: deque[Signals] = deque()
+ self._future: asyncio.Future = asyncio.Future()
+ self._handled_signals: set[Signals] = set()
+
+ def _deliver(self, signum: Signals) -> None:
+ self._signal_queue.append(signum)
+ if not self._future.done():
+ self._future.set_result(None)
+
+ def __enter__(self) -> Self:
+ for sig in set(self._signals):
+ self._loop.add_signal_handler(sig, self._deliver, sig)
+ self._handled_signals.add(sig)
+
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ for sig in self._handled_signals:
+ self._loop.remove_signal_handler(sig)
+
+ def __aiter__(self) -> _SignalReceiver:
+ return self
+
+ async def __anext__(self) -> Signals:
+ await AsyncIOBackend.checkpoint()
+ if not self._signal_queue:
+ self._future = asyncio.Future()
+ await self._future
+
+ return self._signal_queue.popleft()
+
+
+#
+# Testing and debugging
+#
+
+
+class AsyncIOTaskInfo(TaskInfo):
+ def __init__(self, task: asyncio.Task):
+ task_state = _task_states.get(task)
+ if task_state is None:
+ parent_id = None
+ else:
+ parent_id = task_state.parent_id
+
+ coro = task.get_coro()
+ assert coro is not None, "created TaskInfo from a completed Task"
+ super().__init__(id(task), parent_id, task.get_name(), coro)
+ self._task = weakref.ref(task)
+
+ def has_pending_cancellation(self) -> bool:
+ if not (task := self._task()):
+ # If the task isn't around anymore, it won't have a pending cancellation
+ return False
+
+ if task._must_cancel or ( # type: ignore[attr-defined]
+ isinstance(task._fut_waiter, asyncio.Future) # type: ignore[attr-defined]
+ and task._fut_waiter.cancelled() # type: ignore[attr-defined]
+ ):
+ return True
+
+ if task_state := _task_states.get(task):
+ if cancel_scope := task_state.cancel_scope:
+ return cancel_scope._effectively_cancelled
+
+ return False
+
+
+class TestRunner(abc.TestRunner):
+ _send_stream: MemoryObjectSendStream[tuple[Awaitable[Any], asyncio.Future[Any]]]
+
+ def __init__(
+ self,
+ *,
+ debug: bool | None = None,
+ use_uvloop: bool = False,
+ loop_factory: Callable[[], AbstractEventLoop] | None = None,
+ ) -> None:
+ if use_uvloop and loop_factory is None:
+ if sys.platform != "win32":
+ import uvloop
+
+ loop_factory = uvloop.new_event_loop
+ else:
+ import winloop
+
+ loop_factory = winloop.new_event_loop
+
+ self._runner = Runner(debug=debug, loop_factory=loop_factory)
+ self._exceptions: list[BaseException] = []
+ self._runner_task: asyncio.Task | None = None
+
+ def __enter__(self) -> Self:
+ self._runner.__enter__()
+ self.get_loop().set_exception_handler(self._exception_handler)
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self._runner.__exit__(exc_type, exc_val, exc_tb)
+
+ def get_loop(self) -> AbstractEventLoop:
+ return self._runner.get_loop()
+
+ def is_running(self) -> bool:
+ try:
+ asyncio.get_running_loop()
+ return True
+ except RuntimeError:
+ return False
+
+ def _exception_handler(
+ self, loop: asyncio.AbstractEventLoop, context: dict[str, Any]
+ ) -> None:
+ if isinstance(context.get("exception"), Exception):
+ self._exceptions.append(context["exception"])
+ else:
+ loop.default_exception_handler(context)
+
+ def _raise_async_exceptions(self) -> None:
+ # Re-raise any exceptions raised in asynchronous callbacks
+ if self._exceptions:
+ exceptions, self._exceptions = self._exceptions, []
+ if len(exceptions) == 1:
+ raise exceptions[0]
+ elif exceptions:
+ raise BaseExceptionGroup(
+ "Multiple exceptions occurred in asynchronous callbacks", exceptions
+ )
+
+ async def _run_tests_and_fixtures(
+ self,
+ receive_stream: MemoryObjectReceiveStream[
+ tuple[Awaitable[T_Retval], asyncio.Future[T_Retval]]
+ ],
+ ) -> None:
+ from _pytest.outcomes import OutcomeException
+
+ with receive_stream, self._send_stream:
+ async for coro, future in receive_stream:
+ try:
+ retval = await coro
+ except CancelledError as exc:
+ if not future.cancelled():
+ future.cancel(*exc.args)
+
+ raise
+ except BaseException as exc:
+ if not future.cancelled():
+ future.set_exception(exc)
+
+ if not isinstance(exc, (Exception, OutcomeException)):
+ raise
+ else:
+ if not future.cancelled():
+ future.set_result(retval)
+
+ async def _call_in_runner_task(
+ self,
+ func: Callable[P, Awaitable[T_Retval]],
+ /,
+ *args: P.args,
+ **kwargs: P.kwargs,
+ ) -> T_Retval:
+ if not self._runner_task:
+ self._send_stream, receive_stream = create_memory_object_stream[
+ tuple[Awaitable[Any], asyncio.Future]
+ ](1)
+ self._runner_task = self.get_loop().create_task(
+ self._run_tests_and_fixtures(receive_stream)
+ )
+
+ coro = func(*args, **kwargs)
+ future: asyncio.Future[T_Retval] = self.get_loop().create_future()
+ self._send_stream.send_nowait((coro, future))
+ return await future
+
+ def run_asyncgen_fixture(
+ self,
+ fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]],
+ kwargs: dict[str, Any],
+ ) -> Iterable[T_Retval]:
+ asyncgen = fixture_func(**kwargs)
+ fixturevalue: T_Retval = self.get_loop().run_until_complete(
+ self._call_in_runner_task(asyncgen.asend, None)
+ )
+ self._raise_async_exceptions()
+
+ yield fixturevalue
+
+ try:
+ self.get_loop().run_until_complete(
+ self._call_in_runner_task(asyncgen.asend, None)
+ )
+ except StopAsyncIteration:
+ self._raise_async_exceptions()
+ else:
+ self.get_loop().run_until_complete(asyncgen.aclose())
+ raise RuntimeError("Async generator fixture did not stop")
+
+ def run_fixture(
+ self,
+ fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]],
+ kwargs: dict[str, Any],
+ ) -> T_Retval:
+ retval = self.get_loop().run_until_complete(
+ self._call_in_runner_task(fixture_func, **kwargs)
+ )
+ self._raise_async_exceptions()
+ return retval
+
+ def run_test(
+ self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any]
+ ) -> None:
+ from _pytest.outcomes import OutcomeException
+
+ try:
+ self.get_loop().run_until_complete(
+ self._call_in_runner_task(test_func, **kwargs)
+ )
+ except Exception as exc:
+ self._exceptions.append(exc)
+ except OutcomeException:
+ raise
+ except BaseException:
+ # A BaseException (e.g. KeyboardInterrupt, SystemExit) interrupted the event loop before
+ # the test completed. Cancel _runner_task so it does not resume when the event
+ # loop is re-entered during async generator fixture teardown.
+ if self._runner_task is not None and not self._runner_task.done():
+ self._runner_task.cancel()
+ self._send_stream.close()
+ try:
+ self.get_loop().run_until_complete(self._runner_task)
+ except CancelledError:
+ pass
+ finally:
+ self._runner_task = None
+ raise
+ self._raise_async_exceptions()
+
+
+class _ProcessStreamProtocol(asyncio.subprocess.SubprocessStreamProtocol):
+ """
+ A subprocess protocol that allows us to be notified of ``process_exited``
+
+ asyncio's own ``Process.wait()`` only resolves once every pipe transport has
+ disconnected so to get same semantics as on trio and uvloop we need this.
+ """
+
+ def __init__(self) -> None:
+ # Match the standard factory for asyncio.create_process
+ super().__init__(limit=2**16, loop=asyncio.get_running_loop())
+ self.exited = asyncio.Event()
+
+ def process_exited(self) -> None:
+ super().process_exited()
+ self.exited.set()
+
+
+class AsyncIOBackend(AsyncBackend):
+ @classmethod
+ def run(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
+ args: tuple[Unpack[PosArgsT]],
+ kwargs: dict[str, Any],
+ options: dict[str, Any],
+ ) -> T_Retval:
+ @wraps(func)
+ async def wrapper() -> T_Retval:
+ task = cast(asyncio.Task, current_task())
+ task.set_name(get_callable_name(func))
+ _task_states[task] = TaskState(None, None)
+
+ try:
+ return await func(*args)
+ finally:
+ del _task_states[task]
+
+ debug = options.get("debug", None)
+ loop_factory = options.get("loop_factory", None)
+ if loop_factory is None and options.get("use_uvloop", False):
+ if sys.platform != "win32":
+ import uvloop
+
+ loop_factory = uvloop.new_event_loop
+ else:
+ import winloop
+
+ loop_factory = winloop.new_event_loop
+
+ with Runner(debug=debug, loop_factory=loop_factory) as runner:
+ return runner.run(wrapper())
+
+ @classmethod
+ def current_token(cls) -> object:
+ return get_running_loop()
+
+ @classmethod
+ def current_time(cls) -> float:
+ return get_running_loop().time()
+
+ @classmethod
+ def cancelled_exception_class(cls) -> type[BaseException]:
+ return CancelledError
+
+ @classmethod
+ async def checkpoint(cls) -> None:
+ await sleep(0)
+
+ @classmethod
+ async def checkpoint_if_cancelled(cls) -> None:
+ task = current_task()
+ if task is None:
+ return
+
+ try:
+ cancel_scope = _task_states[task].cancel_scope
+ except KeyError:
+ return
+
+ while cancel_scope:
+ if cancel_scope.cancel_called:
+ await sleep(0)
+ elif cancel_scope.shield:
+ break
+ else:
+ cancel_scope = cancel_scope._parent_scope
+
+ @classmethod
+ async def cancel_shielded_checkpoint(cls) -> None:
+ with CancelScope(shield=True):
+ await sleep(0)
+
+ @classmethod
+ async def sleep(cls, delay: float) -> None:
+ await sleep(delay)
+
+ @classmethod
+ def create_cancel_scope(
+ cls, *, deadline: float = math.inf, shield: bool = False
+ ) -> CancelScope:
+ return CancelScope(deadline=deadline, shield=shield)
+
+ @classmethod
+ def current_effective_deadline(cls) -> float:
+ if (task := current_task()) is None:
+ return math.inf
+
+ try:
+ cancel_scope = _task_states[task].cancel_scope
+ except KeyError:
+ return math.inf
+
+ deadline = math.inf
+ while cancel_scope:
+ deadline = min(deadline, cancel_scope.deadline)
+ if cancel_scope._cancel_called:
+ deadline = -math.inf
+ break
+ elif cancel_scope.shield:
+ break
+ else:
+ cancel_scope = cancel_scope._parent_scope
+
+ return deadline
+
+ @classmethod
+ def create_task_group(cls) -> abc.TaskGroup:
+ return TaskGroup()
+
+ @classmethod
+ def create_event(cls) -> BaseEvent:
+ return Event()
+
+ @classmethod
+ def create_lock(cls, *, fast_acquire: bool) -> BaseLock:
+ return Lock(fast_acquire=fast_acquire)
+
+ @classmethod
+ def create_semaphore(
+ cls,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> BaseSemaphore:
+ return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire)
+
+ @classmethod
+ def create_capacity_limiter(cls, total_tokens: float) -> BaseCapacityLimiter:
+ return CapacityLimiter(total_tokens)
+
+ @classmethod
+ async def run_sync_in_worker_thread( # type: ignore[return]
+ cls,
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ abandon_on_cancel: bool = False,
+ limiter: BaseCapacityLimiter | None = None,
+ ) -> T_Retval:
+ await cls.checkpoint()
+
+ # If this is the first run in this event loop thread, set up the necessary
+ # variables
+ try:
+ idle_workers = _threadpool_idle_workers.get()
+ workers = _threadpool_workers.get()
+ except LookupError:
+ idle_workers = deque()
+ workers = set()
+ _threadpool_idle_workers.set(idle_workers)
+ _threadpool_workers.set(workers)
+
+ async with limiter or cls.current_default_thread_limiter():
+ with CancelScope(shield=not abandon_on_cancel) as scope:
+ future = asyncio.Future[T_Retval]()
+ root_task = find_root_task()
+ if not idle_workers:
+ worker = WorkerThread(root_task, workers, idle_workers)
+ worker.start()
+ workers.add(worker)
+ root_task.add_done_callback(worker.stop, context=Context())
+ else:
+ worker = idle_workers.pop()
+
+ # Prune any other workers that have been idle for MAX_IDLE_TIME
+ # seconds or longer
+ now = cls.current_time()
+ while idle_workers:
+ if (
+ now - idle_workers[0].idle_since
+ < WorkerThread.MAX_IDLE_TIME
+ ):
+ break
+
+ expired_worker = idle_workers.popleft()
+ expired_worker.root_task.remove_done_callback(
+ expired_worker.stop
+ )
+ expired_worker.stop()
+
+ context = copy_context()
+ context.run(set_current_async_library, None)
+ if abandon_on_cancel or scope._parent_scope is None:
+ worker_scope = scope
+ else:
+ worker_scope = scope._parent_scope
+
+ worker.queue.put_nowait((context, func, args, future, worker_scope))
+ return await future
+
+ @classmethod
+ def check_cancelled(cls) -> None:
+ scope: CancelScope | None = threadlocals.current_cancel_scope
+ while scope is not None:
+ if scope.cancel_called:
+ raise CancelledError(f"Cancelled via cancel scope {id(scope):x}")
+
+ if scope.shield:
+ return
+
+ scope = scope._parent_scope
+
+ @classmethod
+ def run_async_from_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_co:
+ async def task_wrapper() -> T_co:
+ __tracebackhide__ = True
+ if scope is not None:
+ task = cast(asyncio.Task, current_task())
+ _task_states[task] = TaskState(None, scope)
+ scope._tasks.add(task)
+ try:
+ return await func(*args)
+ except CancelledError as exc:
+ raise concurrent.futures.CancelledError(str(exc)) from None
+ finally:
+ if scope is not None:
+ scope._tasks.discard(task)
+
+ loop = cast(
+ "AbstractEventLoop", token or threadlocals.current_token.native_token
+ )
+ if loop.is_closed():
+ raise RunFinishedError
+
+ context = copy_context()
+ context.run(set_current_async_library, "asyncio")
+ scope = getattr(threadlocals, "current_cancel_scope", None)
+ f: concurrent.futures.Future[T_co] = context.run(
+ asyncio.run_coroutine_threadsafe, task_wrapper(), loop=loop
+ )
+ return f.result()
+
+ @classmethod
+ def run_sync_from_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_Retval:
+ @wraps(func)
+ def wrapper() -> None:
+ try:
+ set_current_async_library("asyncio")
+ f.set_result(func(*args))
+ except BaseException as exc:
+ f.set_exception(exc)
+ if not isinstance(exc, Exception):
+ raise
+
+ loop = cast(
+ "AbstractEventLoop", token or threadlocals.current_token.native_token
+ )
+ if loop.is_closed():
+ raise RunFinishedError
+
+ f: concurrent.futures.Future[T_Retval] = Future()
+ loop.call_soon_threadsafe(wrapper)
+ return f.result()
+
+ @classmethod
+ async def open_process(
+ cls,
+ command: StrOrBytesPath | Sequence[StrOrBytesPath],
+ *,
+ stdin: int | IO[Any] | None,
+ stdout: int | IO[Any] | None,
+ stderr: int | IO[Any] | None,
+ **kwargs: Any,
+ ) -> Process:
+ await cls.checkpoint()
+ if isinstance(command, PathLike):
+ command = os.fspath(command)
+
+ # Use loop.subprocess_shell()/subprocess_exec() rather than their
+ # asyncio.create_subprocess_*() counterparts to get access to
+ # transport/protocol.
+ loop = asyncio.get_running_loop()
+ if isinstance(command, (str, bytes)):
+ transport, protocol = await loop.subprocess_shell(
+ _ProcessStreamProtocol,
+ command,
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ **kwargs,
+ )
+ else:
+ transport, protocol = await loop.subprocess_exec(
+ _ProcessStreamProtocol,
+ *command,
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ **kwargs,
+ )
+
+ process = asyncio.subprocess.Process(transport, protocol, loop)
+ stdin_stream = StreamWriterWrapper(process.stdin) if process.stdin else None
+ stdout_stream = StreamReaderWrapper(process.stdout) if process.stdout else None
+ stderr_stream = StreamReaderWrapper(process.stderr) if process.stderr else None
+ return Process(
+ process,
+ stdin_stream,
+ stdout_stream,
+ stderr_stream,
+ protocol.exited,
+ transport,
+ )
+
+ @classmethod
+ def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None:
+ create_task(
+ _shutdown_process_pool_on_exit(workers),
+ name="AnyIO process pool shutdown task",
+ )
+ find_root_task().add_done_callback(
+ partial(_forcibly_shutdown_process_pool_on_exit, workers) # type:ignore[arg-type]
+ )
+
+ @classmethod
+ async def connect_tcp(
+ cls, host: str, port: int, local_address: IPSockAddrType | None = None
+ ) -> abc.SocketStream:
+ transport, protocol = cast(
+ tuple[asyncio.Transport, StreamProtocol],
+ await get_running_loop().create_connection(
+ StreamProtocol, host, port, local_addr=local_address
+ ),
+ )
+ transport.pause_reading()
+ return SocketStream(transport, protocol)
+
+ @classmethod
+ async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream:
+ await cls.checkpoint()
+ loop = get_running_loop()
+ raw_socket = socket.socket(socket.AF_UNIX)
+ raw_socket.setblocking(False)
+ while True:
+ try:
+ raw_socket.connect(path)
+ except BlockingIOError:
+ f: asyncio.Future = asyncio.Future()
+ loop.add_writer(raw_socket, f.set_result, None)
+ f.add_done_callback(lambda _: loop.remove_writer(raw_socket))
+ await f
+ except BaseException:
+ raw_socket.close()
+ raise
+ else:
+ return UNIXSocketStream(raw_socket)
+
+ @classmethod
+ def create_tcp_listener(cls, sock: socket.socket) -> SocketListener:
+ return TCPSocketListener(sock)
+
+ @classmethod
+ def create_unix_listener(cls, sock: socket.socket) -> SocketListener:
+ return UNIXSocketListener(sock)
+
+ @classmethod
+ async def create_udp_socket(
+ cls,
+ family: AddressFamily,
+ local_address: IPSockAddrType | None,
+ remote_address: IPSockAddrType | None,
+ reuse_port: bool,
+ ) -> UDPSocket | ConnectedUDPSocket:
+ transport, protocol = await get_running_loop().create_datagram_endpoint(
+ DatagramProtocol,
+ local_addr=local_address,
+ remote_addr=remote_address,
+ family=family,
+ reuse_port=reuse_port,
+ )
+ if protocol.exception:
+ transport.close()
+ raise protocol.exception
+
+ if not remote_address:
+ return UDPSocket(transport, protocol)
+ else:
+ return ConnectedUDPSocket(transport, protocol)
+
+ @classmethod
+ async def create_unix_datagram_socket( # type: ignore[override]
+ cls, raw_socket: socket.socket, remote_path: str | bytes | None
+ ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket:
+ await cls.checkpoint()
+ loop = get_running_loop()
+
+ if remote_path:
+ while True:
+ try:
+ raw_socket.connect(remote_path)
+ except BlockingIOError:
+ f: asyncio.Future = asyncio.Future()
+ loop.add_writer(raw_socket, f.set_result, None)
+ f.add_done_callback(lambda _: loop.remove_writer(raw_socket))
+ await f
+ except BaseException:
+ raw_socket.close()
+ raise
+ else:
+ return ConnectedUNIXDatagramSocket(raw_socket)
+ else:
+ return UNIXDatagramSocket(raw_socket)
+
+ @classmethod
+ async def getaddrinfo(
+ cls,
+ host: bytes | str | None,
+ port: str | int | None,
+ *,
+ family: int | AddressFamily = 0,
+ type: int | SocketKind = 0,
+ proto: int = 0,
+ flags: int = 0,
+ ) -> Sequence[
+ tuple[
+ AddressFamily,
+ SocketKind,
+ int,
+ str,
+ tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes],
+ ]
+ ]:
+ return await get_running_loop().getaddrinfo(
+ host, port, family=family, type=type, proto=proto, flags=flags
+ )
+
+ @classmethod
+ async def getnameinfo(
+ cls, sockaddr: IPSockAddrType, flags: int = 0
+ ) -> tuple[str, str]:
+ return await get_running_loop().getnameinfo(sockaddr, flags)
+
+ @classmethod
+ async def wait_readable(cls, obj: FileDescriptorLike) -> None:
+ try:
+ read_events = _read_events.get()
+ except LookupError:
+ read_events = {}
+ _read_events.set(read_events)
+
+ fd = obj if isinstance(obj, int) else obj.fileno()
+ if read_events.get(fd):
+ raise BusyResourceError("reading from")
+
+ loop = get_running_loop()
+ fut: asyncio.Future[bool] = loop.create_future()
+
+ def cb() -> None:
+ try:
+ del read_events[fd]
+ except KeyError:
+ pass
+ else:
+ remove_reader(fd)
+
+ try:
+ fut.set_result(True)
+ except asyncio.InvalidStateError:
+ pass
+
+ try:
+ loop.add_reader(fd, cb)
+ except NotImplementedError:
+ from anyio._core._asyncio_selector_thread import get_selector
+
+ selector = get_selector()
+ selector.add_reader(fd, cb)
+ remove_reader = selector.remove_reader
+ else:
+ remove_reader = loop.remove_reader
+
+ read_events[fd] = fut
+ try:
+ success = await fut
+ finally:
+ try:
+ del read_events[fd]
+ except KeyError:
+ pass
+ else:
+ remove_reader(fd)
+
+ if not success:
+ raise ClosedResourceError
+
+ @classmethod
+ async def wait_writable(cls, obj: FileDescriptorLike) -> None:
+ try:
+ write_events = _write_events.get()
+ except LookupError:
+ write_events = {}
+ _write_events.set(write_events)
+
+ fd = obj if isinstance(obj, int) else obj.fileno()
+ if write_events.get(fd):
+ raise BusyResourceError("writing to")
+
+ loop = get_running_loop()
+ fut: asyncio.Future[bool] = loop.create_future()
+
+ def cb() -> None:
+ try:
+ del write_events[fd]
+ except KeyError:
+ pass
+ else:
+ remove_writer(fd)
+
+ try:
+ fut.set_result(True)
+ except asyncio.InvalidStateError:
+ pass
+
+ try:
+ loop.add_writer(fd, cb)
+ except NotImplementedError:
+ from anyio._core._asyncio_selector_thread import get_selector
+
+ selector = get_selector()
+ selector.add_writer(fd, cb)
+ remove_writer = selector.remove_writer
+ else:
+ remove_writer = loop.remove_writer
+
+ write_events[fd] = fut
+ try:
+ success = await fut
+ finally:
+ try:
+ del write_events[fd]
+ except KeyError:
+ pass
+ else:
+ remove_writer(fd)
+
+ if not success:
+ raise ClosedResourceError
+
+ @classmethod
+ def notify_closing(cls, obj: FileDescriptorLike) -> None:
+ fd = obj if isinstance(obj, int) else obj.fileno()
+ loop = get_running_loop()
+
+ try:
+ write_events = _write_events.get()
+ except LookupError:
+ pass
+ else:
+ try:
+ fut = write_events.pop(fd)
+ except KeyError:
+ pass
+ else:
+ try:
+ fut.set_result(False)
+ except asyncio.InvalidStateError:
+ pass
+
+ try:
+ loop.remove_writer(fd)
+ except NotImplementedError:
+ from anyio._core._asyncio_selector_thread import get_selector
+
+ get_selector().remove_writer(fd)
+
+ try:
+ read_events = _read_events.get()
+ except LookupError:
+ pass
+ else:
+ try:
+ fut = read_events.pop(fd)
+ except KeyError:
+ pass
+ else:
+ try:
+ fut.set_result(False)
+ except asyncio.InvalidStateError:
+ pass
+
+ try:
+ loop.remove_reader(fd)
+ except NotImplementedError:
+ from anyio._core._asyncio_selector_thread import get_selector
+
+ get_selector().remove_reader(fd)
+
+ @classmethod
+ async def wrap_listener_socket(cls, sock: socket.socket) -> SocketListener:
+ if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX:
+ return UNIXSocketListener(sock)
+
+ return TCPSocketListener(sock)
+
+ @classmethod
+ async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream:
+ transport, protocol = await get_running_loop().create_connection(
+ StreamProtocol, sock=sock
+ )
+ return SocketStream(transport, protocol)
+
+ @classmethod
+ async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream:
+ return UNIXSocketStream(sock)
+
+ @classmethod
+ async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket:
+ transport, protocol = await get_running_loop().create_datagram_endpoint(
+ DatagramProtocol, sock=sock
+ )
+ return UDPSocket(transport, protocol)
+
+ @classmethod
+ async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket:
+ transport, protocol = await get_running_loop().create_datagram_endpoint(
+ DatagramProtocol, sock=sock
+ )
+ return ConnectedUDPSocket(transport, protocol)
+
+ @classmethod
+ async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket:
+ return UNIXDatagramSocket(sock)
+
+ @classmethod
+ async def wrap_connected_unix_datagram_socket(
+ cls, sock: socket.socket
+ ) -> ConnectedUNIXDatagramSocket:
+ return ConnectedUNIXDatagramSocket(sock)
+
+ @classmethod
+ def current_default_thread_limiter(cls) -> CapacityLimiter:
+ try:
+ return _default_thread_limiter.get()
+ except LookupError:
+ limiter = CapacityLimiter(40)
+ _default_thread_limiter.set(limiter)
+ return limiter
+
+ @classmethod
+ def open_signal_receiver(
+ cls, *signals: Signals
+ ) -> AbstractContextManager[AsyncIterator[Signals]]:
+ return _SignalReceiver(signals)
+
+ @classmethod
+ def get_current_task(cls) -> TaskInfo:
+ return AsyncIOTaskInfo(current_task()) # type: ignore[arg-type]
+
+ @classmethod
+ def get_running_tasks(cls) -> Sequence[TaskInfo]:
+ return [AsyncIOTaskInfo(task) for task in all_tasks() if not task.done()]
+
+ @classmethod
+ async def wait_all_tasks_blocked(cls) -> None:
+ await cls.checkpoint()
+ this_task = current_task()
+ while True:
+ for task in all_tasks():
+ if task is this_task:
+ continue
+
+ waiter = task._fut_waiter # type: ignore[attr-defined]
+ if waiter is None or waiter.done():
+ await sleep(0.1)
+ break
+ else:
+ return
+
+ @classmethod
+ def create_test_runner(cls, options: dict[str, Any]) -> TestRunner:
+ return TestRunner(**options)
+
+
+backend_class = AsyncIOBackend
diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/_trio.py b/venv/lib/python3.11/site-packages/anyio/_backends/_trio.py
new file mode 100644
index 000000000..2e67786e8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_backends/_trio.py
@@ -0,0 +1,1473 @@
+from __future__ import annotations
+
+import array
+import math
+import os
+import socket
+import sys
+import types
+import weakref
+from collections.abc import (
+ AsyncGenerator,
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Collection,
+ Coroutine,
+ Iterable,
+ Sequence,
+)
+from contextlib import AbstractContextManager
+from contextvars import Context
+from dataclasses import dataclass
+from functools import partial, wraps
+from io import IOBase
+from os import PathLike
+from signal import Signals
+from socket import AddressFamily, SocketKind
+from types import TracebackType
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ Generic,
+ Literal,
+ NoReturn,
+ ParamSpec,
+ TypeVar,
+ cast,
+ overload,
+)
+
+import trio.from_thread
+import trio.lowlevel
+from outcome import Error, Outcome, Value
+from trio.lowlevel import (
+ current_root_task,
+ current_task,
+ notify_closing,
+ wait_readable,
+ wait_writable,
+)
+from trio.socket import SocketType as TrioSocketType
+from trio.to_thread import run_sync
+
+from .. import (
+ CapacityLimiterStatistics,
+ EventStatistics,
+ LockStatistics,
+ RunFinishedError,
+ TaskInfo,
+ WouldBlock,
+ abc,
+)
+from .._core._eventloop import claim_worker_thread
+from .._core._exceptions import (
+ BrokenResourceError,
+ BusyResourceError,
+ ClosedResourceError,
+ EndOfStream,
+)
+from .._core._sockets import convert_ipv6_sockaddr
+from .._core._streams import create_memory_object_stream
+from .._core._synchronization import (
+ CapacityLimiter as BaseCapacityLimiter,
+)
+from .._core._synchronization import Event as BaseEvent
+from .._core._synchronization import Lock as BaseLock
+from .._core._synchronization import (
+ ResourceGuard,
+ SemaphoreStatistics,
+)
+from .._core._synchronization import Semaphore as BaseSemaphore
+from .._core._tasks import CancelScope as BaseCancelScope
+from .._core._tasks import TaskHandle
+from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType
+from ..abc._eventloop import AsyncBackend, StrOrBytesPath
+from ..abc._tasks import T_contra, call_for_coroutine, get_callable_name, get_coro_name
+
+if TYPE_CHECKING:
+ from _typeshed import FileDescriptorLike
+
+ from ..streams.memory import MemoryObjectSendStream
+
+if sys.version_info < (3, 15):
+ from typing_extensions import sentinel
+
+if sys.version_info >= (3, 11):
+ from typing import Self, TypeVarTuple, Unpack
+else:
+ from exceptiongroup import BaseExceptionGroup
+ from typing_extensions import Self, TypeVarTuple, Unpack
+
+T = TypeVar("T")
+T_Retval = TypeVar("T_Retval")
+T_co = TypeVar("T_co", covariant=True)
+T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType)
+PosArgsT = TypeVarTuple("PosArgsT")
+P = ParamSpec("P")
+
+
+def ensure_returns_coro(
+ func: Callable[P, Awaitable[T_Retval]],
+) -> Callable[P, Coroutine[Any, Any, T_Retval]]:
+ @wraps(func)
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Coroutine[Any, Any, T_Retval]:
+ awaitable = func(*args, **kwargs)
+ # Check the common case first.
+ if isinstance(awaitable, Coroutine):
+ return awaitable
+ elif not isinstance(awaitable, Awaitable):
+ # The user violated the type annotations. Still, we should pass this on to
+ # Trio so it can raise with an appropriate message.
+ return awaitable
+ else:
+
+ @wraps(func)
+ async def inner_wrapper() -> T_Retval:
+ return await awaitable
+
+ return inner_wrapper()
+
+ return wrapper
+
+
+#
+# Event loop
+#
+
+RunVar = trio.lowlevel.RunVar
+
+
+#
+# Timeouts and cancellation
+#
+
+
+class CancelScope(BaseCancelScope):
+ __slots__ = ("__original",)
+
+ def __new__(
+ cls, original: trio.CancelScope | None = None, **kwargs: object
+ ) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None:
+ self.__original = original or trio.CancelScope(**kwargs)
+
+ def __enter__(self) -> Self:
+ self.__original.__enter__()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ return self.__original.__exit__(exc_type, exc_val, exc_tb)
+
+ def cancel(self, reason: str | None = None) -> None:
+ self.__original.cancel(reason)
+
+ @property
+ def deadline(self) -> float:
+ return self.__original.deadline
+
+ @deadline.setter
+ def deadline(self, value: float) -> None:
+ self.__original.deadline = value
+
+ @property
+ def cancel_called(self) -> bool:
+ return self.__original.cancel_called
+
+ @property
+ def cancelled_caught(self) -> bool:
+ return self.__original.cancelled_caught
+
+ @property
+ def shield(self) -> bool:
+ return self.__original.shield
+
+ @shield.setter
+ def shield(self, value: bool) -> None:
+ self.__original.shield = value
+
+
+#
+# Task groups
+#
+
+empty_start_value = sentinel("empty_start_value")
+
+
+class _TrioTaskStatus(abc.TaskStatus[T_contra], Generic[T_contra]):
+ early_start_value: T_contra | object = empty_start_value
+ real_task_status: trio.TaskStatus[T_contra | None] | None = None
+
+ def started(self, value: T_contra | None = None) -> None:
+ if self.real_task_status is None:
+ if self.early_start_value is not empty_start_value:
+ raise RuntimeError("called 'started' twice on the same task status")
+
+ self.early_start_value = value
+ else:
+ self.real_task_status.started(value)
+
+
+class TaskGroup(abc.TaskGroup):
+ def __init__(self) -> None:
+ self._entered = False
+ self._active = False
+ self._nursery_manager = trio.open_nursery(strict_exception_groups=True)
+ self.cancel_scope = None # type: ignore[assignment]
+
+ async def __aenter__(self) -> Self:
+ if self._entered:
+ raise RuntimeError("TaskGroup cannot be entered more than once")
+
+ self._entered = True
+ self._active = True
+ self._nursery = await self._nursery_manager.__aenter__()
+ self.cancel_scope = CancelScope(self._nursery.cancel_scope)
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ try:
+ # trio.Nursery.__exit__ returns bool; .open_nursery has wrong type
+ return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[return-value]
+ except BaseExceptionGroup as exc:
+ if not exc.split(trio.Cancelled)[1]:
+ raise trio.Cancelled._create() from exc
+
+ raise
+ finally:
+ del exc_val, exc_tb
+ self._active = False
+
+ def _check_active(self, coro: Coroutine | None = None) -> None:
+ if not self._active:
+ if coro is not None:
+ coro.close()
+
+ raise RuntimeError(
+ "This task group is not active; no new tasks can be started."
+ )
+
+ def create_task(
+ self,
+ coro: Coroutine[Any, Any, T_co],
+ *,
+ name: object = None,
+ context: Context | None = None,
+ ) -> TaskHandle[T_co]:
+ if not isinstance(coro, Coroutine):
+ raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}")
+
+ self._check_active(coro)
+ final_name = get_coro_name(coro, name)
+ handle = TaskHandle(coro, final_name)
+ if context is not None:
+ context.run(
+ partial(self._nursery.start_soon, handle._run_coro, name=handle.name)
+ )
+ else:
+ self._nursery.start_soon(handle._run_coro, name=handle.name)
+
+ return handle
+
+ async def start(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ return_handle: Literal[False, True] = False,
+ ) -> Any:
+ handle: TaskHandle[T_co]
+
+ async def run_coro_with_task_status(
+ *, task_status: trio.TaskStatus[Any]
+ ) -> None:
+ nonlocal handle
+ wrapper_task_status = _TrioTaskStatus()
+ coro = call_for_coroutine(func, args, task_status=wrapper_task_status)
+ if wrapper_task_status.early_start_value is not empty_start_value:
+ task_status.started(wrapper_task_status.early_start_value)
+ else:
+ wrapper_task_status.real_task_status = task_status
+
+ handle = TaskHandle(coro, final_name)
+ await handle._run_coro()
+
+ self._check_active()
+ final_name = get_callable_name(func, name)
+ start_value = await self._nursery.start(
+ run_coro_with_task_status, name=final_name
+ )
+ if return_handle:
+ handle._start_value = start_value
+ return handle
+ else:
+ return start_value
+
+
+#
+# Subprocesses
+#
+
+
+@dataclass(eq=False)
+class ReceiveStreamWrapper(abc.ByteReceiveStream):
+ _stream: trio.abc.ReceiveStream
+
+ async def receive(self, max_bytes: int | None = None) -> bytes:
+ if max_bytes is not None and max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ try:
+ data = await self._stream.receive_some(max_bytes)
+ except trio.ClosedResourceError as exc:
+ raise ClosedResourceError from exc.__cause__
+ except trio.BrokenResourceError as exc:
+ raise BrokenResourceError from exc.__cause__
+
+ if data:
+ return bytes(data)
+ else:
+ raise EndOfStream
+
+ async def aclose(self) -> None:
+ await self._stream.aclose()
+
+
+@dataclass(eq=False)
+class SendStreamWrapper(abc.ByteSendStream):
+ _stream: trio.abc.SendStream
+
+ async def send(self, item: bytes) -> None:
+ try:
+ await self._stream.send_all(item)
+ except trio.ClosedResourceError as exc:
+ raise ClosedResourceError from exc.__cause__
+ except trio.BrokenResourceError as exc:
+ raise BrokenResourceError from exc.__cause__
+
+ async def aclose(self) -> None:
+ await self._stream.aclose()
+
+
+@dataclass(eq=False)
+class Process(abc.Process):
+ _process: trio.Process
+ _stdin: abc.ByteSendStream | None
+ _stdout: abc.ByteReceiveStream | None
+ _stderr: abc.ByteReceiveStream | None
+
+ async def aclose(self) -> None:
+ with CancelScope(shield=True):
+ if self._stdin:
+ await self._stdin.aclose()
+ if self._stdout:
+ await self._stdout.aclose()
+ if self._stderr:
+ await self._stderr.aclose()
+
+ try:
+ await self.wait()
+ except BaseException:
+ self.kill()
+ with CancelScope(shield=True):
+ await self.wait()
+ raise
+
+ async def wait(self) -> int:
+ return await self._process.wait()
+
+ def terminate(self) -> None:
+ self._process.terminate()
+
+ def kill(self) -> None:
+ self._process.kill()
+
+ def send_signal(self, signal: Signals) -> None:
+ self._process.send_signal(signal)
+
+ @property
+ def pid(self) -> int:
+ return self._process.pid
+
+ @property
+ def returncode(self) -> int | None:
+ return self._process.returncode
+
+ @property
+ def stdin(self) -> abc.ByteSendStream | None:
+ return self._stdin
+
+ @property
+ def stdout(self) -> abc.ByteReceiveStream | None:
+ return self._stdout
+
+ @property
+ def stderr(self) -> abc.ByteReceiveStream | None:
+ return self._stderr
+
+
+class _ProcessPoolShutdownInstrument(trio.abc.Instrument):
+ def after_run(self) -> None:
+ super().after_run()
+
+
+current_default_worker_process_limiter: trio.lowlevel.RunVar = RunVar(
+ "current_default_worker_process_limiter"
+)
+
+
+async def _shutdown_process_pool(workers: set[abc.Process]) -> None:
+ try:
+ await trio.sleep(math.inf)
+ except trio.Cancelled:
+ for process in workers:
+ if process.returncode is None:
+ process.kill()
+
+ with CancelScope(shield=True):
+ for process in workers:
+ await process.aclose()
+
+
+#
+# Sockets and networking
+#
+
+
+class _TrioSocketMixin(Generic[T_SockAddr]):
+ def __init__(self, trio_socket: TrioSocketType) -> None:
+ self._trio_socket = trio_socket
+ self._closed = False
+
+ def _check_closed(self) -> None:
+ if self._closed:
+ raise ClosedResourceError
+ if self._trio_socket.fileno() < 0:
+ raise BrokenResourceError
+
+ @property
+ def _raw_socket(self) -> socket.socket:
+ return self._trio_socket._sock # type: ignore[attr-defined]
+
+ async def aclose(self) -> None:
+ if self._trio_socket.fileno() >= 0:
+ self._closed = True
+ self._trio_socket.close()
+
+ def _convert_socket_error(self, exc: BaseException) -> NoReturn:
+ if isinstance(exc, trio.ClosedResourceError):
+ raise ClosedResourceError from exc
+ elif self._trio_socket.fileno() < 0 and self._closed:
+ raise ClosedResourceError from None
+ elif isinstance(exc, OSError):
+ raise BrokenResourceError from exc
+ else:
+ raise exc
+
+
+class SocketStream(_TrioSocketMixin, abc.SocketStream):
+ def __init__(self, trio_socket: TrioSocketType) -> None:
+ super().__init__(trio_socket)
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ with self._receive_guard:
+ try:
+ data = await self._trio_socket.recv(max_bytes)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ if data:
+ return data
+ else:
+ raise EndOfStream
+
+ async def send(self, item: bytes) -> None:
+ with self._send_guard:
+ view = memoryview(item)
+ while view:
+ try:
+ bytes_sent = await self._trio_socket.send(view)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ view = view[bytes_sent:]
+
+ async def send_eof(self) -> None:
+ self._trio_socket.shutdown(socket.SHUT_WR)
+
+
+class UNIXSocketStream(SocketStream, abc.UNIXSocketStream):
+ async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]:
+ if not isinstance(msglen, int) or msglen < 0:
+ raise ValueError("msglen must be a non-negative integer")
+ if not isinstance(maxfds, int) or maxfds < 1:
+ raise ValueError("maxfds must be a positive integer")
+
+ fds = array.array("i")
+ await trio.lowlevel.checkpoint()
+ with self._receive_guard:
+ while True:
+ try:
+ message, ancdata, _flags, _addr = await self._trio_socket.recvmsg(
+ msglen, socket.CMSG_LEN(maxfds * fds.itemsize)
+ )
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+ else:
+ if not message and not ancdata:
+ raise EndOfStream
+
+ break
+
+ for cmsg_level, cmsg_type, cmsg_data in ancdata:
+ if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS:
+ raise RuntimeError(
+ f"Received unexpected ancillary data; message = {message!r}, "
+ f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}"
+ )
+
+ fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
+
+ return message, list(fds)
+
+ async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None:
+ if not message:
+ raise ValueError("message must not be empty")
+ if not fds:
+ raise ValueError("fds must not be empty")
+
+ filenos: list[int] = []
+ for fd in fds:
+ if isinstance(fd, int):
+ filenos.append(fd)
+ elif isinstance(fd, IOBase):
+ filenos.append(fd.fileno())
+
+ fdarray = array.array("i", filenos)
+ await trio.lowlevel.checkpoint()
+ with self._send_guard:
+ while True:
+ try:
+ await self._trio_socket.sendmsg(
+ [message],
+ [
+ (
+ socket.SOL_SOCKET,
+ socket.SCM_RIGHTS,
+ fdarray,
+ )
+ ],
+ )
+ break
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+
+class TCPSocketListener(_TrioSocketMixin, abc.SocketListener):
+ def __init__(self, raw_socket: socket.socket):
+ super().__init__(trio.socket.from_stdlib_socket(raw_socket))
+ self._accept_guard = ResourceGuard("accepting connections from")
+
+ async def accept(self) -> SocketStream:
+ with self._accept_guard:
+ try:
+ trio_socket, _addr = await self._trio_socket.accept()
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ return SocketStream(trio_socket)
+
+
+class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener):
+ def __init__(self, raw_socket: socket.socket):
+ super().__init__(trio.socket.from_stdlib_socket(raw_socket))
+ self._accept_guard = ResourceGuard("accepting connections from")
+
+ async def accept(self) -> UNIXSocketStream:
+ with self._accept_guard:
+ try:
+ trio_socket, _addr = await self._trio_socket.accept()
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ return UNIXSocketStream(trio_socket)
+
+
+class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket):
+ def __init__(self, trio_socket: TrioSocketType) -> None:
+ super().__init__(trio_socket)
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+
+ async def receive(self) -> tuple[bytes, IPSockAddrType]:
+ with self._receive_guard:
+ try:
+ data, addr = await self._trio_socket.recvfrom(65536)
+ return data, convert_ipv6_sockaddr(addr)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ async def send(self, item: UDPPacketType) -> None:
+ with self._send_guard:
+ try:
+ await self._trio_socket.sendto(*item)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+
+class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket):
+ def __init__(self, trio_socket: TrioSocketType) -> None:
+ super().__init__(trio_socket)
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+
+ async def receive(self) -> bytes:
+ with self._receive_guard:
+ try:
+ return await self._trio_socket.recv(65536)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ async def send(self, item: bytes) -> None:
+ with self._send_guard:
+ try:
+ await self._trio_socket.send(item)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+
+class UNIXDatagramSocket(_TrioSocketMixin[str], abc.UNIXDatagramSocket):
+ def __init__(self, trio_socket: TrioSocketType) -> None:
+ super().__init__(trio_socket)
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+
+ async def receive(self) -> UNIXDatagramPacketType:
+ with self._receive_guard:
+ try:
+ data, addr = await self._trio_socket.recvfrom(65536)
+ return data, addr
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ async def send(self, item: UNIXDatagramPacketType) -> None:
+ with self._send_guard:
+ try:
+ await self._trio_socket.sendto(*item)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+
+class ConnectedUNIXDatagramSocket(
+ _TrioSocketMixin[str], abc.ConnectedUNIXDatagramSocket
+):
+ def __init__(self, trio_socket: TrioSocketType) -> None:
+ super().__init__(trio_socket)
+ self._receive_guard = ResourceGuard("reading from")
+ self._send_guard = ResourceGuard("writing to")
+
+ async def receive(self) -> bytes:
+ with self._receive_guard:
+ try:
+ return await self._trio_socket.recv(65536)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+ async def send(self, item: bytes) -> None:
+ with self._send_guard:
+ try:
+ await self._trio_socket.send(item)
+ except BaseException as exc:
+ self._convert_socket_error(exc)
+
+
+#
+# Synchronization
+#
+
+
+class Event(BaseEvent):
+ __slots__ = ("__original",)
+
+ def __new__(cls) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self) -> None:
+ self.__original = trio.Event()
+
+ def is_set(self) -> bool:
+ return self.__original.is_set()
+
+ async def wait(self) -> None:
+ return await self.__original.wait()
+
+ def statistics(self) -> EventStatistics:
+ orig_statistics = self.__original.statistics()
+ return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting)
+
+ def set(self) -> None:
+ self.__original.set()
+
+
+class Lock(BaseLock):
+ __slots__ = "__original", "_fast_acquire"
+
+ def __new__(cls, *, fast_acquire: bool = False) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self, *, fast_acquire: bool = False) -> None:
+ self._fast_acquire = fast_acquire
+ self.__original = trio.Lock()
+
+ @staticmethod
+ def _convert_runtime_error_msg(exc: RuntimeError) -> None:
+ if exc.args == ("attempt to re-acquire an already held Lock",):
+ exc.args = ("Attempted to acquire an already held Lock",)
+
+ async def acquire(self) -> None:
+ if not self._fast_acquire:
+ try:
+ await self.__original.acquire()
+ except RuntimeError as exc:
+ self._convert_runtime_error_msg(exc)
+ raise
+
+ return
+
+ # This is the "fast path" where we don't let other tasks run
+ await trio.lowlevel.checkpoint_if_cancelled()
+ try:
+ self.__original.acquire_nowait()
+ except trio.WouldBlock:
+ await self.__original._lot.park()
+ except RuntimeError as exc:
+ self._convert_runtime_error_msg(exc)
+ raise
+
+ def acquire_nowait(self) -> None:
+ try:
+ self.__original.acquire_nowait()
+ except trio.WouldBlock:
+ raise WouldBlock from None
+ except RuntimeError as exc:
+ self._convert_runtime_error_msg(exc)
+ raise
+
+ def locked(self) -> bool:
+ return self.__original.locked()
+
+ def release(self) -> None:
+ self.__original.release()
+
+ def statistics(self) -> LockStatistics:
+ orig_statistics = self.__original.statistics()
+ owner = TrioTaskInfo(orig_statistics.owner) if orig_statistics.owner else None
+ return LockStatistics(
+ orig_statistics.locked, owner, orig_statistics.tasks_waiting
+ )
+
+
+class Semaphore(BaseSemaphore):
+ __slots__ = ("__original",)
+
+ def __new__(
+ cls,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> Self:
+ return object.__new__(cls)
+
+ def __init__(
+ self,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> None:
+ super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire)
+ self.__original = trio.Semaphore(initial_value, max_value=max_value)
+
+ async def acquire(self) -> None:
+ if not self._fast_acquire:
+ await self.__original.acquire()
+ return
+
+ # This is the "fast path" where we don't let other tasks run
+ await trio.lowlevel.checkpoint_if_cancelled()
+ try:
+ self.__original.acquire_nowait()
+ except trio.WouldBlock:
+ await self.__original._lot.park()
+
+ def acquire_nowait(self) -> None:
+ try:
+ self.__original.acquire_nowait()
+ except trio.WouldBlock:
+ raise WouldBlock from None
+
+ @property
+ def max_value(self) -> int | None:
+ return self.__original.max_value
+
+ @property
+ def value(self) -> int:
+ return self.__original.value
+
+ def release(self) -> None:
+ self.__original.release()
+
+ def statistics(self) -> SemaphoreStatistics:
+ orig_statistics = self.__original.statistics()
+ return SemaphoreStatistics(orig_statistics.tasks_waiting)
+
+
+class CapacityLimiter(BaseCapacityLimiter):
+ __slots__ = ("__original",)
+
+ def __new__(
+ cls,
+ total_tokens: float | None = None,
+ *,
+ original: trio.CapacityLimiter | None = None,
+ ) -> Self:
+ return object.__new__(cls)
+
+ def __init__(
+ self,
+ total_tokens: float | None = None,
+ *,
+ original: trio.CapacityLimiter | None = None,
+ ) -> None:
+ if original is not None:
+ self.__original = original
+ else:
+ assert total_tokens is not None
+ self.__original = trio.CapacityLimiter(total_tokens)
+
+ async def __aenter__(self) -> None:
+ return await self.__original.__aenter__()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ await self.__original.__aexit__(exc_type, exc_val, exc_tb)
+
+ @property
+ def total_tokens(self) -> float:
+ return self.__original.total_tokens
+
+ @total_tokens.setter
+ def total_tokens(self, value: float) -> None:
+ self.__original.total_tokens = value
+
+ @property
+ def borrowed_tokens(self) -> int:
+ return self.__original.borrowed_tokens
+
+ @property
+ def available_tokens(self) -> float:
+ return self.__original.available_tokens
+
+ def acquire_nowait(self) -> None:
+ try:
+ self.__original.acquire_nowait()
+ except trio.WouldBlock:
+ raise WouldBlock from None
+
+ def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
+ try:
+ self.__original.acquire_on_behalf_of_nowait(borrower)
+ except trio.WouldBlock:
+ raise WouldBlock from None
+
+ async def acquire(self) -> None:
+ await self.__original.acquire()
+
+ async def acquire_on_behalf_of(self, borrower: object) -> None:
+ await self.__original.acquire_on_behalf_of(borrower)
+
+ def release(self) -> None:
+ return self.__original.release()
+
+ def release_on_behalf_of(self, borrower: object) -> None:
+ return self.__original.release_on_behalf_of(borrower)
+
+ def statistics(self) -> CapacityLimiterStatistics:
+ orig = self.__original.statistics()
+ return CapacityLimiterStatistics(
+ borrowed_tokens=orig.borrowed_tokens,
+ total_tokens=orig.total_tokens,
+ borrowers=tuple(orig.borrowers),
+ tasks_waiting=orig.tasks_waiting,
+ )
+
+
+_capacity_limiter_wrapper: trio.lowlevel.RunVar = RunVar("_capacity_limiter_wrapper")
+
+
+#
+# Signal handling
+#
+
+
+class _SignalReceiver:
+ _iterator: AsyncIterator[int]
+
+ def __init__(self, signals: tuple[Signals, ...]):
+ self._signals = signals
+
+ def __enter__(self) -> Self:
+ self._cm = trio.open_signal_receiver(*self._signals)
+ self._iterator = self._cm.__enter__()
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool | None:
+ return self._cm.__exit__(exc_type, exc_val, exc_tb)
+
+ def __aiter__(self) -> _SignalReceiver:
+ return self
+
+ async def __anext__(self) -> Signals:
+ signum = await self._iterator.__anext__()
+ return Signals(signum)
+
+
+#
+# Testing and debugging
+#
+
+
+class TestRunner(abc.TestRunner):
+ def __init__(self, **options: Any) -> None:
+ from queue import Queue
+
+ self._call_queue: Queue[Callable[[], object]] = Queue()
+ self._send_stream: (
+ MemoryObjectSendStream[tuple[Awaitable[Any], list[Outcome]]] | None
+ ) = None
+ self._options = options
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: types.TracebackType | None,
+ ) -> None:
+ if self._send_stream:
+ self._send_stream.close()
+ while self._send_stream is not None:
+ self._call_queue.get()()
+
+ def is_running(self) -> bool:
+ return trio.lowlevel.in_trio_task()
+
+ async def _run_tests_and_fixtures(self) -> None:
+ self._send_stream, receive_stream = create_memory_object_stream[
+ tuple[Awaitable[Any], list[Outcome]]
+ ](1)
+ with receive_stream:
+ async for awaitable, outcome_holder in receive_stream:
+ try:
+ retval = await awaitable
+ except BaseException as exc:
+ outcome_holder.append(Error(exc))
+ else:
+ outcome_holder.append(Value(retval))
+
+ def _main_task_finished(self, outcome: object) -> None:
+ self._send_stream = None
+
+ def _call_in_runner_task(
+ self,
+ func: Callable[P, Awaitable[T_Retval]],
+ /,
+ *args: P.args,
+ **kwargs: P.kwargs,
+ ) -> T_Retval:
+ if self._send_stream is None:
+ trio.lowlevel.start_guest_run(
+ self._run_tests_and_fixtures,
+ run_sync_soon_threadsafe=self._call_queue.put,
+ done_callback=self._main_task_finished,
+ **self._options,
+ )
+ while self._send_stream is None:
+ self._call_queue.get()()
+
+ outcome_holder: list[Outcome] = []
+ self._send_stream.send_nowait((func(*args, **kwargs), outcome_holder))
+ while not outcome_holder:
+ self._call_queue.get()()
+
+ return outcome_holder[0].unwrap()
+
+ def run_asyncgen_fixture(
+ self,
+ fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]],
+ kwargs: dict[str, Any],
+ ) -> Iterable[T_Retval]:
+ asyncgen = fixture_func(**kwargs)
+ fixturevalue: T_Retval = self._call_in_runner_task(asyncgen.asend, None)
+
+ yield fixturevalue
+
+ try:
+ self._call_in_runner_task(asyncgen.asend, None)
+ except StopAsyncIteration:
+ pass
+ else:
+ self._call_in_runner_task(asyncgen.aclose)
+ raise RuntimeError("Async generator fixture did not stop")
+
+ def run_fixture(
+ self,
+ fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]],
+ kwargs: dict[str, Any],
+ ) -> T_Retval:
+ return self._call_in_runner_task(fixture_func, **kwargs)
+
+ def run_test(
+ self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any]
+ ) -> None:
+ self._call_in_runner_task(test_func, **kwargs)
+
+
+class TrioTaskInfo(TaskInfo):
+ def __init__(self, task: trio.lowlevel.Task):
+ parent_id = None
+ if task.parent_nursery and task.parent_nursery.parent_task:
+ parent_id = id(task.parent_nursery.parent_task)
+
+ super().__init__(id(task), parent_id, task.name, task.coro)
+ self._task = weakref.proxy(task)
+
+ def has_pending_cancellation(self) -> bool:
+ try:
+ return self._task._cancel_status.effectively_cancelled
+ except ReferenceError:
+ # If the task is no longer around, it surely doesn't have a cancellation
+ # pending
+ return False
+
+
+class TrioBackend(AsyncBackend):
+ @classmethod
+ def run(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
+ args: tuple[Unpack[PosArgsT]],
+ kwargs: dict[str, Any],
+ options: dict[str, Any],
+ ) -> T_Retval:
+ assert not kwargs, "unreachable, and not supported by Trio"
+ return trio.run(ensure_returns_coro(func), *args, **options)
+
+ @classmethod
+ def current_token(cls) -> object:
+ return trio.lowlevel.current_trio_token()
+
+ @classmethod
+ def current_time(cls) -> float:
+ return trio.current_time()
+
+ @classmethod
+ def cancelled_exception_class(cls) -> type[BaseException]:
+ return trio.Cancelled
+
+ @classmethod
+ async def checkpoint(cls) -> None:
+ await trio.lowlevel.checkpoint()
+
+ @classmethod
+ async def checkpoint_if_cancelled(cls) -> None:
+ await trio.lowlevel.checkpoint_if_cancelled()
+
+ @classmethod
+ async def cancel_shielded_checkpoint(cls) -> None:
+ await trio.lowlevel.cancel_shielded_checkpoint()
+
+ @classmethod
+ async def sleep(cls, delay: float) -> None:
+ await trio.sleep(delay)
+
+ @classmethod
+ def create_cancel_scope(
+ cls, *, deadline: float = math.inf, shield: bool = False
+ ) -> BaseCancelScope:
+ return CancelScope(deadline=deadline, shield=shield)
+
+ @classmethod
+ def current_effective_deadline(cls) -> float:
+ return trio.current_effective_deadline()
+
+ @classmethod
+ def create_task_group(cls) -> abc.TaskGroup:
+ return TaskGroup()
+
+ @classmethod
+ def create_event(cls) -> BaseEvent:
+ return Event()
+
+ @classmethod
+ def create_lock(cls, *, fast_acquire: bool) -> Lock:
+ return Lock(fast_acquire=fast_acquire)
+
+ @classmethod
+ def create_semaphore(
+ cls,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> BaseSemaphore:
+ return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire)
+
+ @classmethod
+ def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter:
+ return CapacityLimiter(total_tokens)
+
+ @classmethod
+ async def run_sync_in_worker_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ abandon_on_cancel: bool = False,
+ limiter: BaseCapacityLimiter | None = None,
+ ) -> T_Retval:
+ def wrapper() -> T_Retval:
+ with claim_worker_thread(TrioBackend, token):
+ return func(*args)
+
+ token = TrioBackend.current_token()
+ return await run_sync(
+ wrapper,
+ abandon_on_cancel=abandon_on_cancel,
+ limiter=cast(trio.CapacityLimiter, limiter),
+ )
+
+ @classmethod
+ def check_cancelled(cls) -> None:
+ trio.from_thread.check_cancelled()
+
+ @classmethod
+ def run_async_from_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_co:
+ trio_token = cast("trio.lowlevel.TrioToken | None", token)
+ try:
+ return trio.from_thread.run(func, *args, trio_token=trio_token)
+ except trio.RunFinishedError:
+ raise RunFinishedError from None
+
+ @classmethod
+ def run_sync_from_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_Retval:
+ trio_token = cast("trio.lowlevel.TrioToken | None", token)
+ try:
+ return trio.from_thread.run_sync(func, *args, trio_token=trio_token)
+ except trio.RunFinishedError:
+ raise RunFinishedError from None
+
+ @classmethod
+ async def open_process(
+ cls,
+ command: StrOrBytesPath | Sequence[StrOrBytesPath],
+ *,
+ stdin: int | IO[Any] | None,
+ stdout: int | IO[Any] | None,
+ stderr: int | IO[Any] | None,
+ **kwargs: Any,
+ ) -> Process:
+ def convert_item(item: StrOrBytesPath) -> str:
+ str_or_bytes = os.fspath(item)
+ if isinstance(str_or_bytes, str):
+ return str_or_bytes
+ else:
+ return os.fsdecode(str_or_bytes)
+
+ if isinstance(command, (str, bytes, PathLike)):
+ process = await trio.lowlevel.open_process(
+ convert_item(command),
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ shell=True,
+ **kwargs,
+ )
+ else:
+ process = await trio.lowlevel.open_process(
+ [convert_item(item) for item in command],
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ shell=False,
+ **kwargs,
+ )
+
+ stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None
+ stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None
+ stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None
+ return Process(process, stdin_stream, stdout_stream, stderr_stream)
+
+ @classmethod
+ def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None:
+ trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers)
+
+ @classmethod
+ async def connect_tcp(
+ cls, host: str, port: int, local_address: IPSockAddrType | None = None
+ ) -> SocketStream:
+ family = socket.AF_INET6 if ":" in host else socket.AF_INET
+ trio_socket = trio.socket.socket(family)
+ trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ if local_address:
+ await trio_socket.bind(local_address)
+
+ try:
+ await trio_socket.connect((host, port))
+ except BaseException:
+ trio_socket.close()
+ raise
+
+ return SocketStream(trio_socket)
+
+ @classmethod
+ async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream:
+ trio_socket = trio.socket.socket(socket.AF_UNIX)
+ try:
+ await trio_socket.connect(path)
+ except BaseException:
+ trio_socket.close()
+ raise
+
+ return UNIXSocketStream(trio_socket)
+
+ @classmethod
+ def create_tcp_listener(cls, sock: socket.socket) -> abc.SocketListener:
+ return TCPSocketListener(sock)
+
+ @classmethod
+ def create_unix_listener(cls, sock: socket.socket) -> abc.SocketListener:
+ return UNIXSocketListener(sock)
+
+ @classmethod
+ async def create_udp_socket(
+ cls,
+ family: socket.AddressFamily,
+ local_address: IPSockAddrType | None,
+ remote_address: IPSockAddrType | None,
+ reuse_port: bool,
+ ) -> UDPSocket | ConnectedUDPSocket:
+ trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM)
+
+ if reuse_port:
+ trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+
+ if local_address:
+ await trio_socket.bind(local_address)
+
+ if remote_address:
+ await trio_socket.connect(remote_address)
+ return ConnectedUDPSocket(trio_socket)
+ else:
+ return UDPSocket(trio_socket)
+
+ @classmethod
+ @overload
+ async def create_unix_datagram_socket(
+ cls, raw_socket: socket.socket, remote_path: None
+ ) -> abc.UNIXDatagramSocket: ...
+
+ @classmethod
+ @overload
+ async def create_unix_datagram_socket(
+ cls, raw_socket: socket.socket, remote_path: str | bytes
+ ) -> abc.ConnectedUNIXDatagramSocket: ...
+
+ @classmethod
+ async def create_unix_datagram_socket(
+ cls, raw_socket: socket.socket, remote_path: str | bytes | None
+ ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket:
+ trio_socket = trio.socket.from_stdlib_socket(raw_socket)
+
+ if remote_path:
+ await trio_socket.connect(remote_path)
+ return ConnectedUNIXDatagramSocket(trio_socket)
+ else:
+ return UNIXDatagramSocket(trio_socket)
+
+ @classmethod
+ async def getaddrinfo(
+ cls,
+ host: bytes | str | None,
+ port: str | int | None,
+ *,
+ family: int | AddressFamily = 0,
+ type: int | SocketKind = 0,
+ proto: int = 0,
+ flags: int = 0,
+ ) -> Sequence[
+ tuple[
+ AddressFamily,
+ SocketKind,
+ int,
+ str,
+ tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes],
+ ]
+ ]:
+ return await trio.socket.getaddrinfo(host, port, family, type, proto, flags)
+
+ @classmethod
+ async def getnameinfo(
+ cls, sockaddr: IPSockAddrType, flags: int = 0
+ ) -> tuple[str, str]:
+ return await trio.socket.getnameinfo(sockaddr, flags)
+
+ @classmethod
+ async def wait_readable(cls, obj: FileDescriptorLike) -> None:
+ try:
+ await wait_readable(obj)
+ except trio.ClosedResourceError as exc:
+ raise ClosedResourceError().with_traceback(exc.__traceback__) from None
+ except trio.BusyResourceError:
+ raise BusyResourceError("reading from") from None
+
+ @classmethod
+ async def wait_writable(cls, obj: FileDescriptorLike) -> None:
+ try:
+ await wait_writable(obj)
+ except trio.ClosedResourceError as exc:
+ raise ClosedResourceError().with_traceback(exc.__traceback__) from None
+ except trio.BusyResourceError:
+ raise BusyResourceError("writing to") from None
+
+ @classmethod
+ def notify_closing(cls, obj: FileDescriptorLike) -> None:
+ notify_closing(obj)
+
+ @classmethod
+ async def wrap_listener_socket(cls, sock: socket.socket) -> abc.SocketListener:
+ if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX:
+ return UNIXSocketListener(sock)
+
+ return TCPSocketListener(sock)
+
+ @classmethod
+ async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream:
+ trio_sock = trio.socket.from_stdlib_socket(sock)
+ return SocketStream(trio_sock)
+
+ @classmethod
+ async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream:
+ trio_sock = trio.socket.from_stdlib_socket(sock)
+ return UNIXSocketStream(trio_sock)
+
+ @classmethod
+ async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket:
+ trio_sock = trio.socket.from_stdlib_socket(sock)
+ return UDPSocket(trio_sock)
+
+ @classmethod
+ async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket:
+ trio_sock = trio.socket.from_stdlib_socket(sock)
+ return ConnectedUDPSocket(trio_sock)
+
+ @classmethod
+ async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket:
+ trio_sock = trio.socket.from_stdlib_socket(sock)
+ return UNIXDatagramSocket(trio_sock)
+
+ @classmethod
+ async def wrap_connected_unix_datagram_socket(
+ cls, sock: socket.socket
+ ) -> ConnectedUNIXDatagramSocket:
+ trio_sock = trio.socket.from_stdlib_socket(sock)
+ return ConnectedUNIXDatagramSocket(trio_sock)
+
+ @classmethod
+ def current_default_thread_limiter(cls) -> CapacityLimiter:
+ try:
+ return _capacity_limiter_wrapper.get()
+ except LookupError:
+ limiter = CapacityLimiter(
+ original=trio.to_thread.current_default_thread_limiter()
+ )
+ _capacity_limiter_wrapper.set(limiter)
+ return limiter
+
+ @classmethod
+ def open_signal_receiver(
+ cls, *signals: Signals
+ ) -> AbstractContextManager[AsyncIterator[Signals]]:
+ return _SignalReceiver(signals)
+
+ @classmethod
+ def get_current_task(cls) -> TaskInfo:
+ task = current_task()
+ return TrioTaskInfo(task)
+
+ @classmethod
+ def get_running_tasks(cls) -> Sequence[TaskInfo]:
+ root_task = current_root_task()
+ assert root_task
+ task_infos = [TrioTaskInfo(root_task)]
+ nurseries = root_task.child_nurseries
+ while nurseries:
+ new_nurseries: list[trio.Nursery] = []
+ for nursery in nurseries:
+ for task in nursery.child_tasks:
+ task_infos.append(TrioTaskInfo(task))
+ new_nurseries.extend(task.child_nurseries)
+
+ nurseries = new_nurseries
+
+ return task_infos
+
+ @classmethod
+ async def wait_all_tasks_blocked(cls) -> None:
+ from trio.testing import wait_all_tasks_blocked
+
+ await wait_all_tasks_blocked()
+
+ @classmethod
+ def create_test_runner(cls, options: dict[str, Any]) -> TestRunner:
+ return TestRunner(**options)
+
+
+backend_class = TrioBackend
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__init__.py b/venv/lib/python3.11/site-packages/anyio/_core/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_asyncio_selector_thread.py b/venv/lib/python3.11/site-packages/anyio/_core/_asyncio_selector_thread.py
new file mode 100644
index 000000000..9f35bae56
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_asyncio_selector_thread.py
@@ -0,0 +1,167 @@
+from __future__ import annotations
+
+import asyncio
+import socket
+import threading
+from collections.abc import Callable
+from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from _typeshed import FileDescriptorLike
+
+_selector_lock = threading.Lock()
+_selector: Selector | None = None
+
+
+class Selector:
+ def __init__(self) -> None:
+ self._thread = threading.Thread(target=self.run, name="AnyIO socket selector")
+ self._selector = DefaultSelector()
+ self._send, self._receive = socket.socketpair()
+ self._send.setblocking(False)
+ self._receive.setblocking(False)
+ # This somewhat reduces the amount of memory wasted queueing up data
+ # for wakeups. With these settings, maximum number of 1-byte sends
+ # before getting BlockingIOError:
+ # Linux 4.8: 6
+ # macOS (darwin 15.5): 1
+ # Windows 10: 525347
+ # Windows you're weird. (And on Windows setting SNDBUF to 0 makes send
+ # blocking, even on non-blocking sockets, so don't do that.)
+ self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1)
+ self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1)
+ # On Windows this is a TCP socket so this might matter. On other
+ # platforms this fails b/c AF_UNIX sockets aren't actually TCP.
+ try:
+ self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ except OSError:
+ pass
+
+ self._selector.register(self._receive, EVENT_READ)
+ self._closed = False
+
+ def start(self) -> None:
+ self._thread.start()
+ threading._register_atexit(self._stop) # type: ignore[attr-defined]
+
+ def _stop(self) -> None:
+ global _selector
+ self._closed = True
+ self._notify_self()
+ self._send.close()
+ self._thread.join()
+ self._selector.unregister(self._receive)
+ self._receive.close()
+ self._selector.close()
+ _selector = None
+ assert not self._selector.get_map(), (
+ "selector still has registered file descriptors after shutdown"
+ )
+
+ def _notify_self(self) -> None:
+ try:
+ self._send.send(b"\x00")
+ except BlockingIOError:
+ pass
+
+ def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None:
+ loop = asyncio.get_running_loop()
+ try:
+ key = self._selector.get_key(fd)
+ except KeyError:
+ self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)})
+ else:
+ if EVENT_READ in key.data:
+ raise ValueError(
+ "this file descriptor is already registered for reading"
+ )
+
+ key.data[EVENT_READ] = loop, callback
+ self._selector.modify(fd, key.events | EVENT_READ, key.data)
+
+ self._notify_self()
+
+ def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None:
+ loop = asyncio.get_running_loop()
+ try:
+ key = self._selector.get_key(fd)
+ except KeyError:
+ self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)})
+ else:
+ if EVENT_WRITE in key.data:
+ raise ValueError(
+ "this file descriptor is already registered for writing"
+ )
+
+ key.data[EVENT_WRITE] = loop, callback
+ self._selector.modify(fd, key.events | EVENT_WRITE, key.data)
+
+ self._notify_self()
+
+ def remove_reader(self, fd: FileDescriptorLike) -> bool:
+ try:
+ key = self._selector.get_key(fd)
+ except KeyError:
+ return False
+
+ if new_events := key.events ^ EVENT_READ:
+ del key.data[EVENT_READ]
+ self._selector.modify(fd, new_events, key.data)
+ else:
+ self._selector.unregister(fd)
+
+ return True
+
+ def remove_writer(self, fd: FileDescriptorLike) -> bool:
+ try:
+ key = self._selector.get_key(fd)
+ except KeyError:
+ return False
+
+ if new_events := key.events ^ EVENT_WRITE:
+ del key.data[EVENT_WRITE]
+ self._selector.modify(fd, new_events, key.data)
+ else:
+ self._selector.unregister(fd)
+
+ return True
+
+ def run(self) -> None:
+ while not self._closed:
+ for key, events in self._selector.select():
+ if key.fileobj is self._receive:
+ try:
+ while self._receive.recv(4096):
+ pass
+ except BlockingIOError:
+ pass
+
+ continue
+
+ if events & EVENT_READ:
+ loop, callback = key.data[EVENT_READ]
+ self.remove_reader(key.fd)
+ try:
+ loop.call_soon_threadsafe(callback)
+ except RuntimeError:
+ pass # the loop was already closed
+
+ if events & EVENT_WRITE:
+ loop, callback = key.data[EVENT_WRITE]
+ self.remove_writer(key.fd)
+ try:
+ loop.call_soon_threadsafe(callback)
+ except RuntimeError:
+ pass # the loop was already closed
+
+
+def get_selector() -> Selector:
+ global _selector
+
+ with _selector_lock:
+ if _selector is None:
+ _selector = Selector()
+ _selector.start()
+
+ return _selector
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_concurrency_utils.py b/venv/lib/python3.11/site-packages/anyio/_core/_concurrency_utils.py
new file mode 100644
index 000000000..f66229a57
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_concurrency_utils.py
@@ -0,0 +1,156 @@
+from __future__ import annotations
+
+from inspect import iscoroutine
+
+__all__ = ("amap", "as_completed", "gather")
+
+from collections.abc import AsyncGenerator, Awaitable, Callable, Coroutine, Iterable
+from contextlib import asynccontextmanager
+from typing import TYPE_CHECKING, Any, TypeVar, overload
+
+from ..abc._tasks import get_coro_name
+from ._exceptions import BrokenResourceError
+from ._streams import create_memory_object_stream
+from ._tasks import TaskHandle, create_task_group
+
+if TYPE_CHECKING:
+ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
+
+R = TypeVar("R")
+S = TypeVar("S")
+T = TypeVar("T")
+U = TypeVar("U")
+V = TypeVar("V")
+W = TypeVar("W")
+
+
+@overload
+async def gather(
+ coro1: Coroutine[Any, Any, R], coro2: Coroutine[Any, Any, S], /
+) -> tuple[R, S]: ...
+
+
+@overload
+async def gather(
+ coro1: Coroutine[Any, Any, R],
+ coro2: Coroutine[Any, Any, S],
+ coro3: Coroutine[Any, Any, T],
+ /,
+) -> tuple[R, S, T]: ...
+
+
+@overload
+async def gather(
+ coro1: Coroutine[Any, Any, R],
+ coro2: Coroutine[Any, Any, S],
+ coro3: Coroutine[Any, Any, T],
+ coro4: Coroutine[Any, Any, U],
+ /,
+) -> tuple[R, S, T, U]: ...
+
+
+@overload
+async def gather(
+ coro1: Coroutine[Any, Any, R],
+ coro2: Coroutine[Any, Any, S],
+ coro3: Coroutine[Any, Any, T],
+ coro4: Coroutine[Any, Any, U],
+ coro5: Coroutine[Any, Any, V],
+ /,
+) -> tuple[R, S, T, U, V]: ...
+
+
+@overload
+async def gather(
+ coro1: Coroutine[Any, Any, R],
+ coro2: Coroutine[Any, Any, S],
+ coro3: Coroutine[Any, Any, T],
+ coro4: Coroutine[Any, Any, U],
+ coro5: Coroutine[Any, Any, V],
+ coro6: Coroutine[Any, Any, W],
+ /,
+) -> tuple[R, S, T, U, V, W]: ...
+
+
+# handle arbitrary length if awaitables are all of the same type
+@overload
+async def gather(*coros: Coroutine[Any, Any, R]) -> tuple[R, ...]: ...
+
+
+async def gather(*coros: Coroutine[Any, Any, Any]) -> tuple[Any, ...]:
+ """
+ Run coroutines concurrently in a task group. The order of result values corresponds
+ to the order of coroutines passed.
+
+ :param coros: coroutine objects to run as tasks
+ :return: task results for each argument in the same order as they were passed
+
+ """
+ handles: list[TaskHandle[Any]] = []
+ async with create_task_group() as tg:
+ handles.extend(tg.create_task(coro) for coro in coros)
+
+ return tuple(h.return_value for h in handles)
+
+
+@asynccontextmanager
+async def as_completed(
+ *awaitables: Awaitable[R],
+) -> AsyncGenerator[MemoryObjectReceiveStream[TaskHandle[R]]]:
+ """
+ Run awaitable objects concurrently in a task group, returning an iterator which can
+ be used to get finished task handles in the order they complete.
+
+ :param awaitables: awaitable objects to run as tasks
+ :return: MemoryObjectReceiveStream for iterating over task handles as tasks complete.
+
+ """
+ if not awaitables:
+ raise ValueError("as_completed() takes at least one awaitable")
+
+ send, recv = create_memory_object_stream[TaskHandle[R]](len(awaitables))
+ task_handles: list[TaskHandle[R]] = []
+
+ async def runner(
+ awaitable: Awaitable[R],
+ index: int,
+ _send: MemoryObjectSendStream[TaskHandle[R]],
+ ) -> R:
+ async with _send:
+ try:
+ return await awaitable
+ finally:
+ try:
+ _send.send_nowait(task_handles[index])
+ except BrokenResourceError:
+ pass
+
+ async with recv, create_task_group() as tg:
+ async with send:
+ for i, awaitable in enumerate(awaitables):
+ name = get_coro_name(awaitable) if iscoroutine(awaitable) else None
+ task_handles.append(
+ tg.start_soon(runner, awaitable, i, send.clone(), name=name)
+ )
+ try:
+ yield recv
+ finally:
+ tg.cancel_scope.cancel()
+
+
+async def amap(
+ func: Callable[[T], Coroutine[Any, Any, R]], args: Iterable[T]
+) -> list[R]:
+ """
+ Run the given coroutine function concurrently for multiple argument values.
+
+ :param func: a coroutine function that takes a single argument
+ :param args: a sequence of argument values to pass to ``func``
+ :return: task results for each argument in the same order as they were passed
+
+ """
+ handles: list[TaskHandle[R]] = []
+ async with create_task_group() as tg:
+ handles.extend(tg.start_soon(func, arg) for arg in args)
+
+ return [h.return_value for h in handles]
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_contextmanagers.py b/venv/lib/python3.11/site-packages/anyio/_core/_contextmanagers.py
new file mode 100644
index 000000000..302f32b0c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_contextmanagers.py
@@ -0,0 +1,200 @@
+from __future__ import annotations
+
+from abc import abstractmethod
+from contextlib import AbstractAsyncContextManager, AbstractContextManager
+from inspect import isasyncgen, iscoroutine, isgenerator
+from types import TracebackType
+from typing import Protocol, TypeVar, cast, final
+
+_T_co = TypeVar("_T_co", covariant=True)
+_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound="bool | None")
+
+
+class _SupportsCtxMgr(Protocol[_T_co, _ExitT_co]):
+ def __contextmanager__(self) -> AbstractContextManager[_T_co, _ExitT_co]: ...
+
+
+class _SupportsAsyncCtxMgr(Protocol[_T_co, _ExitT_co]):
+ def __asynccontextmanager__(
+ self,
+ ) -> AbstractAsyncContextManager[_T_co, _ExitT_co]: ...
+
+
+class ContextManagerMixin:
+ """
+ Mixin class providing context manager functionality via a generator-based
+ implementation.
+
+ This class allows you to implement a context manager via :meth:`__contextmanager__`
+ which should return a generator. The mechanics are meant to mirror those of
+ :func:`@contextmanager `.
+
+ .. note:: Classes using this mix-in are not reentrant as context managers, meaning
+ that once you enter it, you can't re-enter before first exiting it.
+
+ .. seealso:: :doc:`contextmanagers`
+ """
+
+ __cm: AbstractContextManager[object, bool | None] | None = None
+
+ @final
+ def __enter__(self: _SupportsCtxMgr[_T_co, bool | None]) -> _T_co:
+ # Needed for mypy to assume self still has the __cm member
+ assert isinstance(self, ContextManagerMixin)
+ if self.__cm is not None:
+ raise RuntimeError(
+ f"this {self.__class__.__qualname__} has already been entered"
+ )
+
+ cm = self.__contextmanager__()
+ if not isinstance(cm, AbstractContextManager):
+ if isgenerator(cm):
+ raise TypeError(
+ "__contextmanager__() returned a generator object instead of "
+ "a context manager. Did you forget to add the @contextmanager "
+ "decorator?"
+ )
+
+ raise TypeError(
+ f"__contextmanager__() did not return a context manager object, "
+ f"but {cm.__class__!r}"
+ )
+
+ if cm is self:
+ raise TypeError(
+ f"{self.__class__.__qualname__}.__contextmanager__() returned "
+ f"self. Did you forget to add the @contextmanager decorator and a "
+ f"'yield' statement?"
+ )
+
+ value = cm.__enter__()
+ self.__cm = cm
+ return value
+
+ @final
+ def __exit__(
+ self: _SupportsCtxMgr[object, _ExitT_co],
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> _ExitT_co:
+ # Needed for mypy to assume self still has the __cm member
+ assert isinstance(self, ContextManagerMixin)
+ if self.__cm is None:
+ raise RuntimeError(
+ f"this {self.__class__.__qualname__} has not been entered yet"
+ )
+
+ # Prevent circular references
+ cm = self.__cm
+ del self.__cm
+
+ return cast(_ExitT_co, cm.__exit__(exc_type, exc_val, exc_tb))
+
+ @abstractmethod
+ def __contextmanager__(self) -> AbstractContextManager[object, bool | None]:
+ """
+ Implement your context manager logic here.
+
+ This method **must** be decorated with
+ :func:`@contextmanager `.
+
+ .. note:: Remember that the ``yield`` will raise any exception raised in the
+ enclosed context block, so use a ``finally:`` block to clean up resources!
+
+ :return: a context manager object
+ """
+
+
+class AsyncContextManagerMixin:
+ """
+ Mixin class providing async context manager functionality via a generator-based
+ implementation.
+
+ This class allows you to implement a context manager via
+ :meth:`__asynccontextmanager__`. The mechanics are meant to mirror those of
+ :func:`@asynccontextmanager `.
+
+ .. note:: Classes using this mix-in are not reentrant as context managers, meaning
+ that once you enter it, you can't re-enter before first exiting it.
+
+ .. seealso:: :doc:`contextmanagers`
+ """
+
+ __cm: AbstractAsyncContextManager[object, bool | None] | None = None
+
+ @final
+ async def __aenter__(self: _SupportsAsyncCtxMgr[_T_co, bool | None]) -> _T_co:
+ # Needed for mypy to assume self still has the __cm member
+ assert isinstance(self, AsyncContextManagerMixin)
+ if self.__cm is not None:
+ raise RuntimeError(
+ f"this {self.__class__.__qualname__} has already been entered"
+ )
+
+ cm = self.__asynccontextmanager__()
+ if not isinstance(cm, AbstractAsyncContextManager):
+ if isasyncgen(cm):
+ raise TypeError(
+ "__asynccontextmanager__() returned an async generator instead of "
+ "an async context manager. Did you forget to add the "
+ "@asynccontextmanager decorator?"
+ )
+ elif iscoroutine(cm):
+ cm.close()
+ raise TypeError(
+ "__asynccontextmanager__() returned a coroutine object instead of "
+ "an async context manager. Did you forget to add the "
+ "@asynccontextmanager decorator and a 'yield' statement?"
+ )
+
+ raise TypeError(
+ f"__asynccontextmanager__() did not return an async context manager, "
+ f"but {cm.__class__!r}"
+ )
+
+ if cm is self:
+ raise TypeError(
+ f"{self.__class__.__qualname__}.__asynccontextmanager__() returned "
+ f"self. Did you forget to add the @asynccontextmanager decorator and a "
+ f"'yield' statement?"
+ )
+
+ value = await cm.__aenter__()
+ self.__cm = cm
+ return value
+
+ @final
+ async def __aexit__(
+ self: _SupportsAsyncCtxMgr[object, _ExitT_co],
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> _ExitT_co:
+ assert isinstance(self, AsyncContextManagerMixin)
+ if self.__cm is None:
+ raise RuntimeError(
+ f"this {self.__class__.__qualname__} has not been entered yet"
+ )
+
+ # Prevent circular references
+ cm = self.__cm
+ del self.__cm
+
+ return cast(_ExitT_co, await cm.__aexit__(exc_type, exc_val, exc_tb))
+
+ @abstractmethod
+ def __asynccontextmanager__(
+ self,
+ ) -> AbstractAsyncContextManager[object, bool | None]:
+ """
+ Implement your async context manager logic here.
+
+ This method **must** be decorated with
+ :func:`@asynccontextmanager `.
+
+ .. note:: Remember that the ``yield`` will raise any exception raised in the
+ enclosed context block, so use a ``finally:`` block to clean up resources!
+
+ :return: an async context manager object
+ """
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py b/venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py
new file mode 100644
index 000000000..a3e2ab1cc
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py
@@ -0,0 +1,240 @@
+from __future__ import annotations
+
+import math
+import sys
+import threading
+from collections.abc import Awaitable, Callable, Generator
+from contextlib import contextmanager
+from contextvars import Token
+from importlib import import_module
+from typing import TYPE_CHECKING, Any, TypeVar
+
+from ._exceptions import NoEventLoopError
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+sniffio: Any
+try:
+ import sniffio
+except ModuleNotFoundError:
+ sniffio = None
+
+if TYPE_CHECKING:
+ from ..abc import AsyncBackend
+
+# This must be updated when new backends are introduced
+BACKENDS = "asyncio", "trio"
+
+T_Retval = TypeVar("T_Retval")
+PosArgsT = TypeVarTuple("PosArgsT")
+
+threadlocals = threading.local()
+loaded_backends: dict[str, type[AsyncBackend]] = {}
+
+
+def run(
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
+ *args: Unpack[PosArgsT],
+ backend: str = "asyncio",
+ backend_options: dict[str, Any] | None = None,
+) -> T_Retval:
+ """
+ Run the given coroutine function in an asynchronous event loop.
+
+ The current thread must not be already running an event loop.
+
+ :param func: a coroutine function
+ :param args: positional arguments to ``func``
+ :param backend: name of the asynchronous event loop implementation – currently
+ either ``asyncio`` or ``trio``
+ :param backend_options: keyword arguments to call the backend ``run()``
+ implementation with (documented :ref:`here `)
+ :return: the return value of the coroutine function
+ :raises RuntimeError: if an asynchronous event loop is already running in this
+ thread
+ :raises LookupError: if the named backend is not found
+
+ """
+ if asynclib_name := current_async_library():
+ raise RuntimeError(f"Already running {asynclib_name} in this thread")
+
+ try:
+ async_backend = get_async_backend(backend)
+ except ImportError as exc:
+ if backend in BACKENDS:
+ raise LookupError(
+ f"Backend {backend!r} is not available. "
+ f"Install it with: pip install anyio[{backend}]"
+ ) from exc
+
+ raise LookupError(f"No such backend: {backend}") from exc
+
+ token = None
+ if asynclib_name is None:
+ # Since we're in control of the event loop, we can cache the name of the async
+ # library
+ token = set_current_async_library(backend)
+
+ try:
+ backend_options = backend_options or {}
+ return async_backend.run(func, args, {}, backend_options)
+ finally:
+ reset_current_async_library(token)
+
+
+async def sleep(delay: float) -> None:
+ """
+ Pause the current task for the specified duration.
+
+ :param delay: the duration, in seconds
+
+ """
+ return await get_async_backend().sleep(delay)
+
+
+async def sleep_forever() -> None:
+ """
+ Pause the current task until it's cancelled.
+
+ This is a shortcut for ``sleep(math.inf)``.
+
+ .. versionadded:: 3.1
+
+ """
+ await sleep(math.inf)
+
+
+async def sleep_until(deadline: float) -> None:
+ """
+ Pause the current task until the given time.
+
+ :param deadline: the absolute time to wake up at (according to the internal
+ monotonic clock of the event loop)
+
+ .. versionadded:: 3.1
+
+ """
+ now = current_time()
+ await sleep(max(deadline - now, 0))
+
+
+def current_time() -> float:
+ """
+ Return the current value of the event loop's internal clock.
+
+ :return: the clock value (seconds)
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().current_time()
+
+
+def get_all_backends() -> tuple[str, ...]:
+ """Return a tuple of the names of all built-in backends."""
+ return BACKENDS
+
+
+def get_available_backends() -> tuple[str, ...]:
+ """
+ Test for the availability of built-in backends.
+
+ :return a tuple of the built-in backend names that were successfully imported
+
+ .. versionadded:: 4.12
+
+ """
+ available_backends: list[str] = []
+ for backend_name in get_all_backends():
+ try:
+ get_async_backend(backend_name)
+ except ImportError:
+ continue
+
+ available_backends.append(backend_name)
+
+ return tuple(available_backends)
+
+
+def get_cancelled_exc_class() -> type[BaseException]:
+ """
+ Return the current async library's cancellation exception class.
+
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().cancelled_exception_class()
+
+
+#
+# Private API
+#
+
+
+@contextmanager
+def claim_worker_thread(
+ backend_class: type[AsyncBackend], token: object
+) -> Generator[Any, None, None]:
+ from ..lowlevel import EventLoopToken
+
+ threadlocals.current_token = EventLoopToken(backend_class, token)
+ try:
+ yield
+ finally:
+ del threadlocals.current_token
+
+
+def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]:
+ if asynclib_name is None:
+ asynclib_name = current_async_library()
+ if not asynclib_name:
+ raise NoEventLoopError(
+ f"Not currently running on any asynchronous event loop. "
+ f"Available async backends: {', '.join(get_all_backends())}"
+ )
+
+ # We use our own dict instead of sys.modules to get the already imported back-end
+ # class because the appropriate modules in sys.modules could potentially be only
+ # partially initialized
+ try:
+ return loaded_backends[asynclib_name]
+ except KeyError:
+ module = import_module(f"anyio._backends._{asynclib_name}")
+ loaded_backends[asynclib_name] = module.backend_class
+ return module.backend_class
+
+
+def current_async_library() -> str | None:
+ if sniffio is None:
+ # If sniffio is not installed, we assume we're either running asyncio or nothing
+ import asyncio
+
+ try:
+ asyncio.get_running_loop()
+ return "asyncio"
+ except RuntimeError:
+ pass
+ else:
+ try:
+ return sniffio.current_async_library()
+ except sniffio.AsyncLibraryNotFoundError:
+ pass
+
+ return None
+
+
+def set_current_async_library(asynclib_name: str | None) -> Token | None:
+ # no-op if sniffio is not installed
+ if sniffio is None:
+ return None
+
+ return sniffio.current_async_library_cvar.set(asynclib_name)
+
+
+def reset_current_async_library(token: Token | None) -> None:
+ if token is not None:
+ sniffio.current_async_library_cvar.reset(token)
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py b/venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py
new file mode 100644
index 000000000..7d9165f53
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py
@@ -0,0 +1,205 @@
+from __future__ import annotations
+
+import sys
+from collections.abc import Generator
+from textwrap import dedent
+from typing import Any
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import BaseExceptionGroup
+
+
+class BrokenResourceError(Exception):
+ """
+ Raised when trying to use a resource that has been rendered unusable due to external
+ causes (e.g. a send stream whose peer has disconnected).
+ """
+
+
+class BrokenWorkerProcess(Exception):
+ """
+ Raised by :meth:`~anyio.to_process.run_sync` if the worker process terminates abruptly or
+ otherwise misbehaves.
+ """
+
+
+class BrokenWorkerInterpreter(Exception):
+ """
+ Raised by :meth:`~anyio.to_interpreter.run_sync` if an unexpected exception is
+ raised in the subinterpreter.
+ """
+
+ def __init__(self, excinfo: Any):
+ # This was adapted from concurrent.futures.interpreter.ExecutionFailed
+ msg = excinfo.formatted
+ if not msg:
+ if excinfo.type and excinfo.msg:
+ msg = f"{excinfo.type.__name__}: {excinfo.msg}"
+ else:
+ msg = excinfo.type.__name__ or excinfo.msg
+
+ super().__init__(msg)
+ self.excinfo = excinfo
+
+ def __str__(self) -> str:
+ try:
+ formatted = self.excinfo.errdisplay
+ except Exception:
+ return super().__str__()
+ else:
+ return dedent(
+ f"""
+ {super().__str__()}
+
+ Uncaught in the interpreter:
+
+ {formatted}
+ """.strip()
+ )
+
+
+class BusyResourceError(Exception):
+ """
+ Raised when two tasks are trying to read from or write to the same resource
+ concurrently.
+ """
+
+ def __init__(self, action: str):
+ super().__init__(f"Another task is already {action} this resource")
+
+
+class ClosedResourceError(Exception):
+ """Raised when trying to use a resource that has been closed."""
+
+
+class ConnectionFailed(OSError):
+ """
+ Raised when a connection attempt fails.
+
+ .. note:: This class inherits from :exc:`OSError` for backwards compatibility.
+ """
+
+
+def iterate_exceptions(
+ exception: BaseException,
+) -> Generator[BaseException, None, None]:
+ if isinstance(exception, BaseExceptionGroup):
+ for exc in exception.exceptions:
+ yield from iterate_exceptions(exc)
+ else:
+ yield exception
+
+
+class DelimiterNotFound(Exception):
+ """
+ Raised during
+ :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the
+ maximum number of bytes has been read without the delimiter being found.
+ """
+
+ def __init__(self, max_bytes: int) -> None:
+ super().__init__(
+ f"The delimiter was not found among the first {max_bytes} bytes"
+ )
+
+
+class EndOfStream(Exception):
+ """
+ Raised when trying to read from a stream that has been closed from the other end.
+ """
+
+
+class IncompleteRead(Exception):
+ """
+ Raised during
+ :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or
+ :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the
+ connection is closed before the requested amount of bytes has been read.
+ """
+
+ def __init__(self) -> None:
+ super().__init__(
+ "The stream was closed before the read operation could be completed"
+ )
+
+
+class TypedAttributeLookupError(LookupError):
+ """
+ Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute
+ is not found and no default value has been given.
+ """
+
+
+class WouldBlock(Exception):
+ """Raised by ``X_nowait`` functions if ``X()`` would block."""
+
+
+class NoEventLoopError(RuntimeError):
+ """
+ Raised by several functions that require an event loop to be running in the current
+ thread when there is no running event loop.
+
+ This is also raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync`
+ if not calling from an AnyIO worker thread, and no ``token`` was passed.
+ """
+
+
+class RunFinishedError(RuntimeError):
+ """
+ Raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` if the event
+ loop associated with the explicitly passed token has already finished.
+ """
+
+ def __init__(self) -> None:
+ super().__init__(
+ "The event loop associated with the given token has already finished"
+ )
+
+
+class TaskFailed(Exception):
+ """
+ Raised when awaiting on, or attempting to access the return value of, a
+ :class:`.TaskHandle` that raised an exception.
+ """
+
+
+class TaskCancelled(TaskFailed):
+ """
+ Raised when awaiting on, or attempting to access the return value of, a
+ :class:`.TaskHandle` that was cancelled.
+ """
+
+
+class TaskNotFinished(Exception):
+ """
+ Raised when attempting to access the return value or exception of a
+ :class:`.TaskHandle` that is still pending completion.
+ """
+
+
+class FutureFailed(Exception):
+ """
+ Raised when awaiting on, or attempting to access the return value of, a
+ :class:`.Future` that raised an exception.
+ """
+
+
+class FutureCancelled(FutureFailed):
+ """
+ Raised when attempting to access the return value or exception of a
+ :class:`.Future` that was cancelled.
+ """
+
+
+class FutureNotFinished(Exception):
+ """
+ Raised when attempting to access the return value or exception of a
+ :class:`.Future` that is still pending completion.
+ """
+
+
+class FutureAlreadyFinished(Exception):
+ """
+ Raised when attempting set a result of or await a
+ :class:`.Future` that has already completed.
+ """
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_fileio.py b/venv/lib/python3.11/site-packages/anyio/_core/_fileio.py
new file mode 100644
index 000000000..498eef41d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_fileio.py
@@ -0,0 +1,1045 @@
+from __future__ import annotations
+
+import os
+import pathlib
+import sys
+from collections.abc import (
+ AsyncIterator,
+ Callable,
+ Iterable,
+ Iterator,
+ Sequence,
+)
+from dataclasses import dataclass
+from functools import partial
+from os import PathLike
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ AnyStr,
+ ClassVar,
+ Final,
+ Generic,
+ TypeVar,
+ overload,
+)
+
+from .. import to_thread
+from ..abc import AsyncResource
+from ._synchronization import CapacityLimiter
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
+
+if sys.version_info >= (3, 14):
+ from pathlib.types import PathInfo
+
+if TYPE_CHECKING:
+ from types import ModuleType
+
+ from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer
+else:
+ ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object
+
+
+T = TypeVar("T", bound="Path")
+
+
+class AsyncFile(AsyncResource, Generic[AnyStr]):
+ """
+ An asynchronous file object.
+
+ This class wraps a standard file object and provides async friendly versions of the
+ following blocking methods (where available on the original file object):
+
+ * read
+ * read1
+ * readline
+ * readlines
+ * readinto
+ * readinto1
+ * write
+ * writelines
+ * truncate
+ * seek
+ * tell
+ * flush
+
+ All other methods are directly passed through.
+
+ This class supports the asynchronous context manager protocol which closes the
+ underlying file at the end of the context block.
+
+ This class also supports asynchronous iteration::
+
+ async with await open_file(...) as f:
+ async for line in f:
+ print(line)
+ """
+
+ def __init__(
+ self, fp: IO[AnyStr], *, limiter: CapacityLimiter | None = None
+ ) -> None:
+ if limiter is not None and not isinstance(limiter, CapacityLimiter):
+ raise TypeError(
+ f"limiter must be a CapacityLimiter or None, not "
+ f"{limiter.__class__.__name__}"
+ )
+
+ self._fp: Any = fp
+ self._limiter = limiter
+
+ def __getattr__(self, name: str) -> object:
+ return getattr(self._fp, name)
+
+ @property
+ def limiter(self) -> CapacityLimiter | None:
+ """The capacity limiter used by this file object, if not the global limiter."""
+ return self._limiter
+
+ @property
+ def wrapped(self) -> IO[AnyStr]:
+ """The wrapped file object."""
+ return self._fp
+
+ async def __aiter__(self) -> AsyncIterator[AnyStr]:
+ while True:
+ line = await self.readline()
+ if line:
+ yield line
+ else:
+ break
+
+ async def aclose(self) -> None:
+ return await to_thread.run_sync(self._fp.close, limiter=self._limiter)
+
+ async def read(self, size: int = -1) -> AnyStr:
+ return await to_thread.run_sync(self._fp.read, size, limiter=self._limiter)
+
+ async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes:
+ return await to_thread.run_sync(self._fp.read1, size, limiter=self._limiter)
+
+ async def readline(self) -> AnyStr:
+ return await to_thread.run_sync(self._fp.readline, limiter=self._limiter)
+
+ async def readlines(self) -> list[AnyStr]:
+ return await to_thread.run_sync(self._fp.readlines, limiter=self._limiter)
+
+ async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int:
+ return await to_thread.run_sync(self._fp.readinto, b, limiter=self._limiter)
+
+ async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int:
+ return await to_thread.run_sync(self._fp.readinto1, b, limiter=self._limiter)
+
+ @overload
+ async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ...
+
+ @overload
+ async def write(self: AsyncFile[str], b: str) -> int: ...
+
+ async def write(self, b: ReadableBuffer | str) -> int:
+ return await to_thread.run_sync(self._fp.write, b, limiter=self._limiter)
+
+ @overload
+ async def writelines(
+ self: AsyncFile[bytes], lines: Iterable[ReadableBuffer]
+ ) -> None: ...
+
+ @overload
+ async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ...
+
+ async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None:
+ return await to_thread.run_sync(
+ self._fp.writelines, lines, limiter=self._limiter
+ )
+
+ async def truncate(self, size: int | None = None) -> int:
+ return await to_thread.run_sync(self._fp.truncate, size, limiter=self._limiter)
+
+ async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int:
+ return await to_thread.run_sync(
+ self._fp.seek, offset, whence, limiter=self._limiter
+ )
+
+ async def tell(self) -> int:
+ return await to_thread.run_sync(self._fp.tell, limiter=self._limiter)
+
+ async def flush(self) -> None:
+ return await to_thread.run_sync(self._fp.flush, limiter=self._limiter)
+
+
+@overload
+async def open_file(
+ file: str | PathLike[str] | int,
+ mode: OpenBinaryMode,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ errors: str | None = ...,
+ newline: str | None = ...,
+ closefd: bool = ...,
+ opener: Callable[[str, int], int] | None = ...,
+ *,
+ limiter: CapacityLimiter | None = ...,
+) -> AsyncFile[bytes]: ...
+
+
+@overload
+async def open_file(
+ file: str | PathLike[str] | int,
+ mode: OpenTextMode = ...,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ errors: str | None = ...,
+ newline: str | None = ...,
+ closefd: bool = ...,
+ opener: Callable[[str, int], int] | None = ...,
+ *,
+ limiter: CapacityLimiter | None = ...,
+) -> AsyncFile[str]: ...
+
+
+async def open_file(
+ file: str | PathLike[str] | int,
+ mode: str = "r",
+ buffering: int = -1,
+ encoding: str | None = None,
+ errors: str | None = None,
+ newline: str | None = None,
+ closefd: bool = True,
+ opener: Callable[[str, int], int] | None = None,
+ *,
+ limiter: CapacityLimiter | None = None,
+) -> AsyncFile[Any]:
+ """
+ Open a file asynchronously.
+
+ Except for ``limiter``, the arguments are exactly the same as for the builtin :func:`open`.
+
+ :param limiter: an optional capacity limiter to use with the file
+ instead of the default one
+ :return: an asynchronous file object
+
+ .. versionchanged:: 4.14.0
+ Added the ``limiter`` keyword argument.
+
+ """
+ fp = await to_thread.run_sync(
+ open,
+ file,
+ mode,
+ buffering,
+ encoding,
+ errors,
+ newline,
+ closefd,
+ opener,
+ limiter=limiter,
+ )
+ return AsyncFile(fp, limiter=limiter)
+
+
+def wrap_file(
+ file: IO[AnyStr], *, limiter: CapacityLimiter | None = None
+) -> AsyncFile[AnyStr]:
+ """
+ Wrap an existing file as an asynchronous file.
+
+ :param file: an existing file-like object
+ :param limiter: an optional capacity limiter to use with the file
+ instead of the default one
+ :return: an asynchronous file object
+
+ .. versionchanged:: 4.14.0
+ Added the ``limiter`` keyword argument.
+
+ """
+ return AsyncFile(file, limiter=limiter)
+
+
+@dataclass(eq=False)
+class _PathIterator(AsyncIterator[T]):
+ iterator: Iterator[PathLike[str]]
+ limiter: CapacityLimiter | None
+ # This was added to ensure that iterating over a subclass of Path yields instances
+ # of that subclass rather than the base Path class.
+ path_cls: type[T]
+
+ async def __anext__(self) -> T:
+ nextval = await to_thread.run_sync(
+ next, self.iterator, None, abandon_on_cancel=True, limiter=self.limiter
+ )
+ if nextval is None:
+ raise StopAsyncIteration from None
+
+ return self.path_cls(nextval, limiter=self.limiter)
+
+
+class Path:
+ """
+ An asynchronous version of :class:`pathlib.Path`.
+
+ This class cannot be substituted for :class:`pathlib.Path` or
+ :class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike`
+ interface.
+
+ It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for
+ the deprecated :meth:`~pathlib.Path.link_to` method.
+
+ Some methods may be unavailable or have limited functionality, based on the Python
+ version:
+
+ * :meth:`~pathlib.Path.copy` (available on Python 3.14 or later)
+ * :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later)
+ * :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later)
+ * :meth:`~pathlib.PurePath.full_match` (available on Python 3.13 or later)
+ * :attr:`~pathlib.Path.info` (available on Python 3.14 or later)
+ * :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later)
+ * :meth:`~pathlib.PurePath.match` (the ``case_sensitive`` parameter is only
+ available on Python 3.13 or later)
+ * :meth:`~pathlib.Path.move` (available on Python 3.14 or later)
+ * :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later)
+ * :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available
+ on Python 3.12 or later)
+ * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later)
+
+ Any methods that do disk I/O need to be awaited on. These methods are:
+
+ * :meth:`~pathlib.Path.absolute`
+ * :meth:`~pathlib.Path.chmod`
+ * :meth:`~pathlib.Path.cwd`
+ * :meth:`~pathlib.Path.exists`
+ * :meth:`~pathlib.Path.expanduser`
+ * :meth:`~pathlib.Path.group`
+ * :meth:`~pathlib.Path.hardlink_to`
+ * :meth:`~pathlib.Path.home`
+ * :meth:`~pathlib.Path.is_block_device`
+ * :meth:`~pathlib.Path.is_char_device`
+ * :meth:`~pathlib.Path.is_dir`
+ * :meth:`~pathlib.Path.is_fifo`
+ * :meth:`~pathlib.Path.is_file`
+ * :meth:`~pathlib.Path.is_junction`
+ * :meth:`~pathlib.Path.is_mount`
+ * :meth:`~pathlib.Path.is_socket`
+ * :meth:`~pathlib.Path.is_symlink`
+ * :meth:`~pathlib.Path.lchmod`
+ * :meth:`~pathlib.Path.lstat`
+ * :meth:`~pathlib.Path.mkdir`
+ * :meth:`~pathlib.Path.open`
+ * :meth:`~pathlib.Path.owner`
+ * :meth:`~pathlib.Path.read_bytes`
+ * :meth:`~pathlib.Path.read_text`
+ * :meth:`~pathlib.Path.readlink`
+ * :meth:`~pathlib.Path.rename`
+ * :meth:`~pathlib.Path.replace`
+ * :meth:`~pathlib.Path.resolve`
+ * :meth:`~pathlib.Path.rmdir`
+ * :meth:`~pathlib.Path.samefile`
+ * :meth:`~pathlib.Path.stat`
+ * :meth:`~pathlib.Path.symlink_to`
+ * :meth:`~pathlib.Path.touch`
+ * :meth:`~pathlib.Path.unlink`
+ * :meth:`~pathlib.Path.walk`
+ * :meth:`~pathlib.Path.write_bytes`
+ * :meth:`~pathlib.Path.write_text`
+
+ Additionally, the following methods return an async iterator yielding
+ :class:`~.Path` objects:
+
+ * :meth:`~pathlib.Path.glob`
+ * :meth:`~pathlib.Path.iterdir`
+ * :meth:`~pathlib.Path.rglob`
+
+ .. versionchanged:: 4.14.0
+ Added the ``limiter`` keyword argument.
+ """
+
+ __slots__ = "__weakref__", "_limiter", "_path"
+
+ __weakref__: Any
+
+ def __init__(
+ self, *args: str | PathLike[str], limiter: CapacityLimiter | None = None
+ ) -> None:
+ if limiter is not None and not isinstance(limiter, CapacityLimiter):
+ raise TypeError(
+ f"limiter must be a CapacityLimiter or None, not "
+ f"{limiter.__class__.__name__}"
+ )
+
+ self._path: Final[pathlib.Path] = pathlib.Path(*args)
+ self._limiter = limiter
+
+ def __fspath__(self) -> str:
+ return self._path.__fspath__()
+
+ if sys.version_info >= (3, 15):
+
+ def __vfspath__(self) -> str:
+ return self._path.__vfspath__()
+
+ def __str__(self) -> str:
+ return self._path.__str__()
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self.as_posix()!r})"
+
+ def __bytes__(self) -> bytes:
+ return self._path.__bytes__()
+
+ def __hash__(self) -> int:
+ return self._path.__hash__()
+
+ def __eq__(self, other: object) -> bool:
+ target = other._path if isinstance(other, Path) else other
+ return self._path.__eq__(target)
+
+ def __lt__(self, other: pathlib.PurePath | Path) -> bool:
+ target = other._path if isinstance(other, Path) else other
+ return self._path.__lt__(target)
+
+ def __le__(self, other: pathlib.PurePath | Path) -> bool:
+ target = other._path if isinstance(other, Path) else other
+ return self._path.__le__(target)
+
+ def __gt__(self, other: pathlib.PurePath | Path) -> bool:
+ target = other._path if isinstance(other, Path) else other
+ return self._path.__gt__(target)
+
+ def __ge__(self, other: pathlib.PurePath | Path) -> bool:
+ target = other._path if isinstance(other, Path) else other
+ return self._path.__ge__(target)
+
+ def __truediv__(self, other: str | PathLike[str]) -> Self:
+ return type(self)(self._path / other, limiter=self._limiter)
+
+ def __rtruediv__(self, other: str | PathLike[str]) -> Self:
+ return type(self)(other, limiter=self._limiter) / self
+
+ @property
+ def limiter(self) -> CapacityLimiter | None:
+ """The capacity limiter used by this path, if not the global limiter."""
+ return self._limiter
+
+ @property
+ def parts(self) -> tuple[str, ...]:
+ return self._path.parts
+
+ @property
+ def drive(self) -> str:
+ return self._path.drive
+
+ @property
+ def root(self) -> str:
+ return self._path.root
+
+ @property
+ def anchor(self) -> str:
+ return self._path.anchor
+
+ @property
+ def parents(self) -> Sequence[Self]:
+ return tuple(type(self)(p, limiter=self._limiter) for p in self._path.parents)
+
+ @property
+ def parent(self) -> Self:
+ return type(self)(self._path.parent, limiter=self._limiter)
+
+ @property
+ def name(self) -> str:
+ return self._path.name
+
+ @property
+ def suffix(self) -> str:
+ return self._path.suffix
+
+ @property
+ def suffixes(self) -> list[str]:
+ return self._path.suffixes
+
+ @property
+ def stem(self) -> str:
+ return self._path.stem
+
+ async def absolute(self) -> Self:
+ path = await to_thread.run_sync(self._path.absolute, limiter=self._limiter)
+ return type(self)(path, limiter=self._limiter)
+
+ def as_posix(self) -> str:
+ return self._path.as_posix()
+
+ def as_uri(self) -> str:
+ return self._path.as_uri()
+
+ if sys.version_info >= (3, 13):
+ parser: ClassVar[ModuleType] = pathlib.Path.parser
+
+ @classmethod
+ def from_uri(cls, uri: str, *, limiter: CapacityLimiter | None = None) -> Self:
+ return cls(pathlib.Path.from_uri(uri), limiter=limiter)
+
+ def full_match(
+ self, path_pattern: str, *, case_sensitive: bool | None = None
+ ) -> bool:
+ return self._path.full_match(path_pattern, case_sensitive=case_sensitive)
+
+ def match(
+ self, path_pattern: str, *, case_sensitive: bool | None = None
+ ) -> bool:
+ return self._path.match(path_pattern, case_sensitive=case_sensitive)
+ else:
+
+ def match(self, path_pattern: str) -> bool:
+ return self._path.match(path_pattern)
+
+ if sys.version_info >= (3, 14):
+
+ @property
+ def info(self) -> PathInfo:
+ return self._path.info
+
+ async def copy(
+ self,
+ target: str | os.PathLike[str],
+ *,
+ follow_symlinks: bool = True,
+ preserve_metadata: bool = False,
+ ) -> Self:
+ func = partial(
+ self._path.copy,
+ follow_symlinks=follow_symlinks,
+ preserve_metadata=preserve_metadata,
+ )
+ return type(self)(
+ await to_thread.run_sync(
+ func, pathlib.Path(target), limiter=self._limiter
+ ),
+ limiter=self._limiter,
+ )
+
+ async def copy_into(
+ self,
+ target_dir: str | os.PathLike[str],
+ *,
+ follow_symlinks: bool = True,
+ preserve_metadata: bool = False,
+ ) -> Self:
+ func = partial(
+ self._path.copy_into,
+ follow_symlinks=follow_symlinks,
+ preserve_metadata=preserve_metadata,
+ )
+ return type(self)(
+ await to_thread.run_sync(
+ func, pathlib.Path(target_dir), limiter=self._limiter
+ ),
+ limiter=self._limiter,
+ )
+
+ async def move(self, target: str | os.PathLike[str]) -> Self:
+ # Upstream does not handle anyio.Path properly as a PathLike
+ target = pathlib.Path(target)
+ return type(self)(
+ await to_thread.run_sync(
+ self._path.move, target, limiter=self._limiter
+ ),
+ limiter=self._limiter,
+ )
+
+ async def move_into(
+ self,
+ target_dir: str | os.PathLike[str],
+ ) -> Self:
+ return type(self)(
+ await to_thread.run_sync(
+ self._path.move_into, target_dir, limiter=self._limiter
+ ),
+ limiter=self._limiter,
+ )
+
+ def is_relative_to(self, other: str | PathLike[str]) -> bool:
+ try:
+ self.relative_to(other)
+ return True
+ except ValueError:
+ return False
+
+ async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None:
+ func = partial(os.chmod, follow_symlinks=follow_symlinks)
+ return await to_thread.run_sync(func, self._path, mode, limiter=self._limiter)
+
+ @classmethod
+ async def cwd(cls, *, limiter: CapacityLimiter | None = None) -> Self:
+ path = await to_thread.run_sync(pathlib.Path.cwd, limiter=limiter)
+ return cls(path, limiter=limiter)
+
+ if sys.version_info >= (3, 12):
+
+ async def exists(self, *, follow_symlinks: bool = True) -> bool:
+ return await to_thread.run_sync(
+ partial(self._path.exists, follow_symlinks=follow_symlinks),
+ abandon_on_cancel=True,
+ limiter=self._limiter,
+ )
+
+ else:
+
+ async def exists(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.exists, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def expanduser(self) -> Self:
+ return type(self)(
+ await to_thread.run_sync(
+ self._path.expanduser, abandon_on_cancel=True, limiter=self._limiter
+ ),
+ limiter=self._limiter,
+ )
+
+ if sys.version_info < (3, 12):
+ # Python 3.11 and earlier
+ def glob(self, pattern: str) -> AsyncIterator[Self]:
+ gen = self._path.glob(pattern)
+ return _PathIterator(gen, self._limiter, type(self))
+ elif (3, 12) <= sys.version_info < (3, 13):
+ # changed in Python 3.12:
+ # - The case_sensitive parameter was added.
+ def glob(
+ self,
+ pattern: str,
+ *,
+ case_sensitive: bool | None = None,
+ ) -> AsyncIterator[Self]:
+ gen = self._path.glob(pattern, case_sensitive=case_sensitive)
+ return _PathIterator(gen, self._limiter, type(self))
+ elif sys.version_info >= (3, 13):
+ # Changed in Python 3.13:
+ # - The recurse_symlinks parameter was added.
+ # - The pattern parameter accepts a path-like object.
+ def glob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block
+ self,
+ pattern: str | PathLike[str],
+ *,
+ case_sensitive: bool | None = None,
+ recurse_symlinks: bool = False,
+ ) -> AsyncIterator[Self]:
+ gen = self._path.glob(
+ pattern, # type: ignore[arg-type]
+ case_sensitive=case_sensitive,
+ recurse_symlinks=recurse_symlinks,
+ )
+ return _PathIterator(gen, self._limiter, type(self))
+
+ if sys.version_info >= (3, 13):
+
+ async def group(self, *, follow_symlinks: bool = True) -> str:
+ return await to_thread.run_sync(
+ partial(self._path.group, follow_symlinks=follow_symlinks),
+ abandon_on_cancel=True,
+ limiter=self._limiter,
+ )
+
+ else:
+
+ async def group(self) -> str:
+ return await to_thread.run_sync(
+ self._path.group, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def hardlink_to(
+ self, target: str | bytes | PathLike[str] | PathLike[bytes]
+ ) -> None:
+ if isinstance(target, Path):
+ target = target._path
+
+ await to_thread.run_sync(os.link, target, self, limiter=self._limiter)
+
+ @classmethod
+ async def home(cls, *, limiter: CapacityLimiter | None = None) -> Self:
+ home_path = await to_thread.run_sync(pathlib.Path.home, limiter=limiter)
+ return cls(home_path, limiter=limiter)
+
+ def is_absolute(self) -> bool:
+ return self._path.is_absolute()
+
+ async def is_block_device(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_block_device, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def is_char_device(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_char_device, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ if sys.version_info >= (3, 13):
+
+ async def is_dir(self, *, follow_symlinks: bool = True) -> bool:
+ return await to_thread.run_sync(
+ partial(self._path.is_dir, follow_symlinks=follow_symlinks),
+ abandon_on_cancel=True,
+ limiter=self._limiter,
+ )
+
+ else:
+
+ async def is_dir(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_dir, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def is_fifo(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_fifo, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ if sys.version_info >= (3, 13):
+
+ async def is_file(self, *, follow_symlinks: bool = True) -> bool:
+ return await to_thread.run_sync(
+ partial(self._path.is_file, follow_symlinks=follow_symlinks),
+ abandon_on_cancel=True,
+ limiter=self._limiter,
+ )
+
+ else:
+
+ async def is_file(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_file, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ if sys.version_info >= (3, 12):
+
+ async def is_junction(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_junction, limiter=self._limiter
+ )
+
+ async def is_mount(self) -> bool:
+ return await to_thread.run_sync(
+ os.path.ismount, self._path, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ if sys.version_info < (3, 15):
+
+ def is_reserved(self) -> bool:
+ return self._path.is_reserved()
+
+ async def is_socket(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_socket, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def is_symlink(self) -> bool:
+ return await to_thread.run_sync(
+ self._path.is_symlink, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def iterdir(self) -> AsyncIterator[Self]:
+ gen = (
+ self._path.iterdir()
+ if sys.version_info < (3, 13)
+ else await to_thread.run_sync(
+ self._path.iterdir, abandon_on_cancel=True, limiter=self._limiter
+ )
+ )
+ async for path in _PathIterator(gen, self._limiter, type(self)):
+ yield path
+
+ def joinpath(self, *args: str | PathLike[str]) -> Self:
+ return type(self)(self._path.joinpath(*args), limiter=self._limiter)
+
+ async def lchmod(self, mode: int) -> None:
+ await to_thread.run_sync(self._path.lchmod, mode, limiter=self._limiter)
+
+ async def lstat(self) -> os.stat_result:
+ return await to_thread.run_sync(
+ self._path.lstat, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def mkdir(
+ self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False
+ ) -> None:
+ await to_thread.run_sync(
+ self._path.mkdir, mode, parents, exist_ok, limiter=self._limiter
+ )
+
+ @overload
+ async def open(
+ self,
+ mode: OpenBinaryMode,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ errors: str | None = ...,
+ newline: str | None = ...,
+ ) -> AsyncFile[bytes]: ...
+
+ @overload
+ async def open(
+ self,
+ mode: OpenTextMode = ...,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ errors: str | None = ...,
+ newline: str | None = ...,
+ ) -> AsyncFile[str]: ...
+
+ async def open(
+ self,
+ mode: str = "r",
+ buffering: int = -1,
+ encoding: str | None = None,
+ errors: str | None = None,
+ newline: str | None = None,
+ ) -> AsyncFile[Any]:
+ fp = await to_thread.run_sync(
+ self._path.open,
+ mode,
+ buffering,
+ encoding,
+ errors,
+ newline,
+ limiter=self._limiter,
+ )
+ return AsyncFile(fp, limiter=self._limiter)
+
+ if sys.version_info >= (3, 13):
+
+ async def owner(self, *, follow_symlinks: bool = True) -> str:
+ return await to_thread.run_sync(
+ partial(self._path.owner, follow_symlinks=follow_symlinks),
+ abandon_on_cancel=True,
+ limiter=self._limiter,
+ )
+
+ else:
+
+ async def owner(self) -> str:
+ return await to_thread.run_sync(
+ self._path.owner, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def read_bytes(self) -> bytes:
+ return await to_thread.run_sync(self._path.read_bytes, limiter=self._limiter)
+
+ if sys.version_info >= (3, 13):
+
+ async def read_text(
+ self,
+ encoding: str | None = None,
+ errors: str | None = None,
+ newline: str | None = None,
+ ) -> str:
+ return await to_thread.run_sync(
+ self._path.read_text,
+ encoding,
+ errors,
+ newline,
+ limiter=self._limiter,
+ )
+
+ else:
+
+ async def read_text(
+ self, encoding: str | None = None, errors: str | None = None
+ ) -> str:
+ return await to_thread.run_sync(
+ self._path.read_text, encoding, errors, limiter=self._limiter
+ )
+
+ if sys.version_info >= (3, 12):
+
+ def relative_to(
+ self, *other: str | PathLike[str], walk_up: bool = False
+ ) -> Self:
+ # relative_to() should work with any PathLike but it doesn't
+ others = [pathlib.Path(other) for other in other]
+ return type(self)(
+ self._path.relative_to(*others, walk_up=walk_up), limiter=self._limiter
+ )
+
+ else:
+
+ def relative_to(self, *other: str | PathLike[str]) -> Self:
+ return type(self)(self._path.relative_to(*other), limiter=self._limiter)
+
+ async def readlink(self) -> Self:
+ target = await to_thread.run_sync(
+ os.readlink, self._path, limiter=self._limiter
+ )
+ return type(self)(target, limiter=self._limiter)
+
+ async def rename(self, target: str | pathlib.PurePath | Path) -> Self:
+ if isinstance(target, Path):
+ target = target._path
+
+ await to_thread.run_sync(self._path.rename, target, limiter=self._limiter)
+ return type(self)(target, limiter=self._limiter)
+
+ async def replace(self, target: str | pathlib.PurePath | Path) -> Self:
+ if isinstance(target, Path):
+ target = target._path
+
+ await to_thread.run_sync(self._path.replace, target, limiter=self._limiter)
+ return type(self)(target, limiter=self._limiter)
+
+ async def resolve(self, strict: bool = False) -> Self:
+ func = partial(self._path.resolve, strict=strict)
+ return type(self)(
+ await to_thread.run_sync(
+ func, abandon_on_cancel=True, limiter=self._limiter
+ ),
+ limiter=self._limiter,
+ )
+
+ if sys.version_info < (3, 12):
+ # Pre Python 3.12
+ def rglob(self, pattern: str) -> AsyncIterator[Self]:
+ gen = self._path.rglob(pattern)
+ return _PathIterator(gen, self._limiter, type(self))
+ elif (3, 12) <= sys.version_info < (3, 13):
+ # Changed in Python 3.12:
+ # - The case_sensitive parameter was added.
+ def rglob(
+ self, pattern: str, *, case_sensitive: bool | None = None
+ ) -> AsyncIterator[Self]:
+ gen = self._path.rglob(pattern, case_sensitive=case_sensitive)
+ return _PathIterator(gen, self._limiter, type(self))
+ elif sys.version_info >= (3, 13):
+ # Changed in Python 3.13:
+ # - The recurse_symlinks parameter was added.
+ # - The pattern parameter accepts a path-like object.
+ def rglob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block
+ self,
+ pattern: str | PathLike[str],
+ *,
+ case_sensitive: bool | None = None,
+ recurse_symlinks: bool = False,
+ ) -> AsyncIterator[Self]:
+ gen = self._path.rglob(
+ pattern, # type: ignore[arg-type]
+ case_sensitive=case_sensitive,
+ recurse_symlinks=recurse_symlinks,
+ )
+ return _PathIterator(gen, self._limiter, type(self))
+
+ async def rmdir(self) -> None:
+ await to_thread.run_sync(self._path.rmdir, limiter=self._limiter)
+
+ async def samefile(self, other_path: str | PathLike[str]) -> bool:
+ if isinstance(other_path, Path):
+ other_path = other_path._path
+
+ return await to_thread.run_sync(
+ self._path.samefile,
+ other_path,
+ abandon_on_cancel=True,
+ limiter=self._limiter,
+ )
+
+ async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result:
+ func = partial(os.stat, follow_symlinks=follow_symlinks)
+ return await to_thread.run_sync(
+ func, self._path, abandon_on_cancel=True, limiter=self._limiter
+ )
+
+ async def symlink_to(
+ self,
+ target: str | bytes | PathLike[str] | PathLike[bytes],
+ target_is_directory: bool = False,
+ ) -> None:
+ if isinstance(target, Path):
+ target = target._path
+
+ await to_thread.run_sync(
+ self._path.symlink_to, target, target_is_directory, limiter=self._limiter
+ )
+
+ async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None:
+ await to_thread.run_sync(
+ self._path.touch, mode, exist_ok, limiter=self._limiter
+ )
+
+ async def unlink(self, missing_ok: bool = False) -> None:
+ try:
+ await to_thread.run_sync(self._path.unlink, limiter=self._limiter)
+ except FileNotFoundError:
+ if not missing_ok:
+ raise
+
+ if sys.version_info >= (3, 12):
+
+ async def walk(
+ self,
+ top_down: bool = True,
+ on_error: Callable[[OSError], object] | None = None,
+ follow_symlinks: bool = False,
+ ) -> AsyncIterator[tuple[Self, list[str], list[str]]]:
+ def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None:
+ try:
+ return next(gen)
+ except StopIteration:
+ return None
+
+ gen = self._path.walk(top_down, on_error, follow_symlinks)
+ while True:
+ value = await to_thread.run_sync(get_next_value, limiter=self._limiter)
+ if value is None:
+ return
+
+ root, dirs, paths = value
+ yield type(self)(root, limiter=self._limiter), dirs, paths
+
+ def with_name(self, name: str) -> Self:
+ return type(self)(self._path.with_name(name), limiter=self._limiter)
+
+ if sys.version_info < (3, 13):
+ # Backport pathlib's Python>=3.13 behavior for empty stems on paths with non-empty suffixes.
+ # See: https://github.com/python/cpython/pull/114612
+ def with_stem(self, stem: str) -> Self:
+ suffix = self._path.suffix
+ if not suffix:
+ return self.with_name(stem)
+ elif not stem:
+ # If the suffix is non-empty, we can't make the stem empty.
+ raise ValueError(f"{self!r} has a non-empty suffix")
+ else:
+ return self.with_name(stem + suffix)
+ else:
+
+ def with_stem(self, stem: str) -> Self:
+ return type(self)(self._path.with_stem(stem), limiter=self._limiter)
+
+ def with_suffix(self, suffix: str) -> Self:
+ return type(self)(self._path.with_suffix(suffix), limiter=self._limiter)
+
+ def with_segments(self, *pathsegments: str | PathLike[str]) -> Self:
+ return type(self)(*pathsegments, limiter=self._limiter)
+
+ async def write_bytes(self, data: ReadableBuffer) -> int:
+ return await to_thread.run_sync(
+ self._path.write_bytes, data, limiter=self._limiter
+ )
+
+ async def write_text(
+ self,
+ data: str,
+ encoding: str | None = None,
+ errors: str | None = None,
+ newline: str | None = None,
+ ) -> int:
+ return await to_thread.run_sync(
+ self._path.write_text,
+ data,
+ encoding,
+ errors,
+ newline,
+ limiter=self._limiter,
+ )
+
+
+PathLike.register(Path)
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_futures.py b/venv/lib/python3.11/site-packages/anyio/_core/_futures.py
new file mode 100644
index 000000000..7730c53c7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_futures.py
@@ -0,0 +1,191 @@
+from __future__ import annotations
+
+from collections.abc import Generator
+from enum import Enum, auto
+from typing import Any, Generic, TypeVar
+
+from ._exceptions import (
+ FutureAlreadyFinished,
+ FutureCancelled,
+ FutureFailed,
+ FutureNotFinished,
+)
+from ._synchronization import Event
+
+T = TypeVar("T")
+
+
+class Future(Generic[T]):
+ """
+ An awaitable object that works similar to a :class:`asyncio.Future` but
+ with similar characteristics to a :class:`.TaskHandle`.
+ """
+
+ class Status(Enum):
+ """
+ The status of a future handle.
+
+ .. attribute:: PENDING
+
+ The future has not finished yet.
+ .. attribute:: FINISHED
+
+ The future has finished with a return value.
+ .. attribute:: CANCELLED
+
+ The future was cancelled and has finished since.
+ .. attribute:: FAILED
+
+ The future raised an exception.
+ """
+
+ PENDING = auto()
+ FINISHED = auto()
+ CANCELLED = auto()
+ FAILED = auto()
+
+ __slots__ = (
+ "_cancelled",
+ "_exception",
+ "_finished_event",
+ "_name",
+ "_return_value",
+ )
+ _return_value: T
+
+ def __init__(self, *, name: str | None = None) -> None:
+ self._finished_event = Event()
+ self._exception: BaseException | None = None
+ self._cancelled: bool = False
+ self._name = name
+
+ def _check_pending(self) -> None:
+ """Shortcut for checking if a Future is pending
+
+ :raises FutureAlreadyFinished: if future was already given a result or exception.
+ :raises FutureCancelled: if future has been cancelled previously.
+ """
+ match self.status:
+ case Future.Status.PENDING:
+ return
+ case Future.Status.FINISHED:
+ raise FutureAlreadyFinished("future has already finished")
+ case Future.Status.FAILED:
+ raise FutureAlreadyFinished("future already failed")
+ case Future.Status.CANCELLED:
+ raise FutureCancelled("future was cancelled")
+
+ async def wait(self) -> None:
+ """
+ Waits for the future to finish.
+
+ This method will attempt to wait for a result or exception
+ """
+ await self._finished_event.wait()
+
+ def cancel(self) -> None:
+ """Cancels a pending `.Future` object
+
+ Does nothing if the Future was already finished.
+ """
+ if self.status is Future.Status.PENDING:
+ self._cancelled = True
+ self._finished_event.set()
+
+ @property
+ def exception(self) -> BaseException | None:
+ """
+ The exception value of a `.Future`
+
+ :raises FutureNotFinished: if future is still pending
+ :raises FutureCancelled: if future was cancelled
+ :returns: None if future succeeds with a result sent instead
+ otherwise this will be an exception
+ """
+
+ match self.status:
+ case Future.Status.PENDING:
+ raise FutureNotFinished("the future has not finished yet")
+ case Future.Status.FINISHED:
+ return None
+ case Future.Status.CANCELLED:
+ raise FutureCancelled("the future was cancelled")
+ case Future.Status.FAILED:
+ return self._exception
+
+ @exception.setter
+ def exception(self, exception: BaseException) -> None:
+ """Send exception for a `.Future` object
+
+ :raises FutureAlreadyFinished: if future was already given a result or exception.
+ :raises FutureCancelled: if future has been cancelled previously.
+ """
+ self._check_pending()
+ self._exception = exception
+ self._finished_event.set()
+
+ @property
+ def return_value(self) -> T:
+ """
+ The return value of the future.
+
+ :raises FutureNotFinished: if the future has not finished yet
+ :raises FutureCancelled: if the future was cancelled
+ :raises FutureFailed: if the future raised an exception
+
+ """
+ match self.status:
+ case Future.Status.PENDING:
+ raise FutureNotFinished("the future has not finished yet")
+ case Future.Status.FINISHED:
+ return self._return_value
+ case Future.Status.CANCELLED:
+ raise FutureCancelled("the future was cancelled")
+ case Future.Status.FAILED:
+ raise FutureFailed(
+ "the future raised an exception"
+ ) from self._exception
+
+ @return_value.setter
+ def return_value(self, value: T) -> None:
+ """
+ Send pending result for a `.Future` object
+
+ :raises FutureAlreadyFinished: if future was already given a result or exception.
+ :raises FutureCancelled: if future has been cancelled previously.
+ """
+ self._check_pending()
+ self._return_value = value
+ self._finished_event.set()
+
+ @property
+ def status(self) -> Future.Status:
+ """
+ The current status of a future.
+
+ Every future starts in the :attr:`~Future.Status.PENDING` state.
+ If a future is cancelled while in this state, it will transition to the
+ :attr:`Future.Status.CANCELLED` state. When the task finishes, it will
+ transition to one of the three final states (
+ :attr:`Future.Status.FINISHED`, :attr:`Future.Status.FAILED`, or
+ :attr:`Future.Status.CANCELLED`) depending on the exception the task
+ raised, if any. No other status transitions will happen.
+ """
+ if not self._finished_event.is_set():
+ return Future.Status.PENDING
+ elif self._cancelled:
+ return Future.Status.CANCELLED
+ elif self._exception is not None:
+ return Future.Status.FAILED
+ else:
+ return Future.Status.FINISHED
+
+ def __await__(self) -> Generator[Any, Any, T]:
+ yield from self.wait().__await__()
+ return self.return_value
+
+ def __repr__(self) -> str:
+ return (
+ f"<{self.__class__.__name__} {self.status.name.lower()} "
+ f"name={self._name!r}>"
+ )
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_resources.py b/venv/lib/python3.11/site-packages/anyio/_core/_resources.py
new file mode 100644
index 000000000..6ca35d94a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_resources.py
@@ -0,0 +1,18 @@
+from __future__ import annotations
+
+from ..abc import AsyncResource # noqa: TC001
+from ._tasks import CancelScope
+
+
+async def aclose_forcefully(resource: AsyncResource) -> None:
+ """
+ Close an asynchronous resource in a cancelled scope.
+
+ Doing this closes the resource without waiting on anything.
+
+ :param resource: the resource to close
+
+ """
+ with CancelScope() as scope:
+ scope.cancel()
+ await resource.aclose()
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_signals.py b/venv/lib/python3.11/site-packages/anyio/_core/_signals.py
new file mode 100644
index 000000000..e24c79e10
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_signals.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+from collections.abc import AsyncIterator
+from contextlib import AbstractContextManager
+from signal import Signals
+
+from ._eventloop import get_async_backend
+
+
+def open_signal_receiver(
+ *signals: Signals,
+) -> AbstractContextManager[AsyncIterator[Signals]]:
+ """
+ Start receiving operating system signals.
+
+ :param signals: signals to receive (e.g. ``signal.SIGINT``)
+ :return: an asynchronous context manager for an asynchronous iterator which yields
+ signal numbers
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ .. warning:: Windows does not support signals natively so it is best to avoid
+ relying on this in cross-platform applications.
+
+ .. warning:: On asyncio, this permanently replaces any previous signal handler for
+ the given signals, as set via :meth:`~asyncio.loop.add_signal_handler`.
+
+ """
+ return get_async_backend().open_signal_receiver(*signals)
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_sockets.py b/venv/lib/python3.11/site-packages/anyio/_core/_sockets.py
new file mode 100644
index 000000000..14ee6e41e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_sockets.py
@@ -0,0 +1,1011 @@
+from __future__ import annotations
+
+import errno
+import os
+import socket
+import ssl
+import stat
+import sys
+from collections.abc import Awaitable
+from dataclasses import dataclass
+from ipaddress import IPv4Address, IPv6Address, ip_address
+from os import PathLike, chmod
+from socket import AddressFamily, SocketKind
+from typing import TYPE_CHECKING, Any, Literal, cast, overload
+
+from .. import ConnectionFailed, to_thread
+from ..abc import (
+ ByteStreamConnectable,
+ ConnectedUDPSocket,
+ ConnectedUNIXDatagramSocket,
+ IPAddressType,
+ IPSockAddrType,
+ SocketListener,
+ SocketStream,
+ UDPSocket,
+ UNIXDatagramSocket,
+ UNIXSocketStream,
+)
+from ..streams.stapled import MultiListener
+from ..streams.tls import TLSConnectable, TLSStream
+from ._eventloop import get_async_backend
+from ._resources import aclose_forcefully
+from ._synchronization import Event
+from ._tasks import create_task_group, move_on_after
+
+if TYPE_CHECKING:
+ from _typeshed import FileDescriptorLike
+else:
+ FileDescriptorLike = object
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
+if sys.version_info >= (3, 12):
+ from typing import override
+else:
+ from typing_extensions import override
+
+if sys.version_info < (3, 13):
+ from typing_extensions import deprecated
+else:
+ from warnings import deprecated
+
+IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515
+
+AnyIPAddressFamily = Literal[
+ AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6
+]
+IPAddressFamily = Literal[AddressFamily.AF_INET, AddressFamily.AF_INET6]
+
+
+def idna2008_resolve(host: str) -> bytes:
+ try:
+ return host.encode("ascii")
+ except UnicodeEncodeError:
+ import idna
+
+ return idna.encode(host, uts46=True)
+
+
+# tls_hostname given
+@overload
+async def connect_tcp(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ local_host: IPAddressType | None = ...,
+ local_port: int | None = ...,
+ ssl_context: ssl.SSLContext | None = ...,
+ tls_standard_compatible: bool = ...,
+ tls_hostname: str,
+ happy_eyeballs_delay: float = ...,
+) -> TLSStream: ...
+
+
+# ssl_context given
+@overload
+async def connect_tcp(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ local_host: IPAddressType | None = ...,
+ local_port: int | None = ...,
+ ssl_context: ssl.SSLContext,
+ tls_standard_compatible: bool = ...,
+ tls_hostname: str | None = ...,
+ happy_eyeballs_delay: float = ...,
+) -> TLSStream: ...
+
+
+# tls=True
+@overload
+async def connect_tcp(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ local_host: IPAddressType | None = ...,
+ local_port: int | None = ...,
+ tls: Literal[True],
+ ssl_context: ssl.SSLContext | None = ...,
+ tls_standard_compatible: bool = ...,
+ tls_hostname: str | None = ...,
+ happy_eyeballs_delay: float = ...,
+) -> TLSStream: ...
+
+
+# tls=False
+@overload
+async def connect_tcp(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ local_host: IPAddressType | None = ...,
+ local_port: int | None = ...,
+ tls: Literal[False],
+ ssl_context: ssl.SSLContext | None = ...,
+ tls_standard_compatible: bool = ...,
+ tls_hostname: str | None = ...,
+ happy_eyeballs_delay: float = ...,
+) -> SocketStream: ...
+
+
+# No TLS arguments
+@overload
+async def connect_tcp(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ local_host: IPAddressType | None = ...,
+ local_port: int | None = ...,
+ happy_eyeballs_delay: float = ...,
+) -> SocketStream: ...
+
+
+async def connect_tcp(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ local_host: IPAddressType | None = None,
+ local_port: int | None = None,
+ tls: bool = False,
+ ssl_context: ssl.SSLContext | None = None,
+ tls_standard_compatible: bool = True,
+ tls_hostname: str | None = None,
+ happy_eyeballs_delay: float = 0.25,
+) -> SocketStream | TLSStream:
+ """
+ Connect to a host using the TCP protocol.
+
+ This function implements the stateless version of the Happy Eyeballs algorithm (RFC
+ 6555). If ``remote_host`` is a host name that resolves to multiple IP addresses,
+ each one is tried until one connection attempt succeeds. If the first attempt does
+ not connected within 250 milliseconds, a second attempt is started using the next
+ address in the list, and so on. On IPv6 enabled systems, an IPv6 address (if
+ available) is tried first.
+
+ When the connection has been established, a TLS handshake will be done if either
+ ``ssl_context`` or ``tls_hostname`` is not ``None``, or if ``tls`` is ``True``.
+
+ :param remote_host: the IP address or host name to connect to
+ :param remote_port: port on the target host to connect to
+ :param local_host: the interface address or name to bind the socket to before
+ connecting
+ :param local_port: the local port to bind to (requires ``local_host`` to also be
+ set)
+ :param tls: ``True`` to do a TLS handshake with the connected stream and return a
+ :class:`~anyio.streams.tls.TLSStream` instead
+ :param ssl_context: the SSL context object to use (if omitted, a default context is
+ created)
+ :param tls_standard_compatible: If ``True``, performs the TLS shutdown handshake
+ before closing the stream and requires that the server does this as well.
+ Otherwise, :exc:`~ssl.SSLEOFError` may be raised during reads from the stream.
+ Some protocols, such as HTTP, require this option to be ``False``.
+ See :meth:`~ssl.SSLContext.wrap_socket` for details.
+ :param tls_hostname: host name to check the server certificate against (defaults to
+ the value of ``remote_host``)
+ :param happy_eyeballs_delay: delay (in seconds) before starting the next connection
+ attempt
+ :return: a socket stream object if no TLS handshake was done, otherwise a TLS stream
+ :raises ConnectionFailed: if the connection fails
+
+ """
+ # Placed here due to https://github.com/python/mypy/issues/7057
+ connected_stream: SocketStream | None = None
+
+ async def try_connect(remote_host: str, event: Event) -> None:
+ nonlocal connected_stream
+ try:
+ stream = await asynclib.connect_tcp(remote_host, remote_port, local_address)
+ except OSError as exc:
+ oserrors.append(exc)
+ return
+ else:
+ if connected_stream is None:
+ connected_stream = stream
+ tg.cancel_scope.cancel()
+ else:
+ await stream.aclose()
+ finally:
+ event.set()
+
+ asynclib = get_async_backend()
+ local_address: IPSockAddrType | None = None
+ family = socket.AF_UNSPEC
+ if local_host:
+ gai_res = await getaddrinfo(str(local_host), local_port)
+ family, *_, local_address = gai_res[0]
+
+ target_host = str(remote_host)
+ try:
+ addr_obj = ip_address(remote_host)
+ except ValueError:
+ addr_obj = None
+
+ if addr_obj is not None:
+ if isinstance(addr_obj, IPv6Address):
+ target_addrs = [(socket.AF_INET6, addr_obj.compressed)]
+ else:
+ target_addrs = [(socket.AF_INET, addr_obj.compressed)]
+ else:
+ # getaddrinfo() will raise an exception if name resolution fails
+ gai_res = await getaddrinfo(
+ target_host, remote_port, family=family, type=socket.SOCK_STREAM
+ )
+
+ # Organize the list so that the first address is an IPv6 address (if available)
+ # and the second one is an IPv4 addresses. The rest can be in whatever order.
+ v6_found = v4_found = False
+ target_addrs = []
+ for af, *_, sa in gai_res:
+ if af == socket.AF_INET6 and not v6_found:
+ v6_found = True
+ target_addrs.insert(0, (af, sa[0]))
+ elif af == socket.AF_INET and not v4_found and v6_found:
+ v4_found = True
+ target_addrs.insert(1, (af, sa[0]))
+ else:
+ target_addrs.append((af, sa[0]))
+
+ oserrors: list[OSError] = []
+ try:
+ async with create_task_group() as tg:
+ for _af, addr in target_addrs:
+ event = Event()
+ tg.start_soon(try_connect, addr, event)
+ with move_on_after(happy_eyeballs_delay):
+ await event.wait()
+
+ if connected_stream is None:
+ cause = (
+ oserrors[0]
+ if len(oserrors) == 1
+ else ExceptionGroup("multiple connection attempts failed", oserrors)
+ )
+ raise OSError("All connection attempts failed") from cause
+ finally:
+ oserrors.clear()
+
+ if tls or tls_hostname or ssl_context:
+ try:
+ return await TLSStream.wrap(
+ connected_stream,
+ server_side=False,
+ hostname=tls_hostname or str(remote_host),
+ ssl_context=ssl_context,
+ standard_compatible=tls_standard_compatible,
+ )
+ except BaseException:
+ await aclose_forcefully(connected_stream)
+ raise
+
+ return connected_stream
+
+
+async def connect_unix(path: str | bytes | PathLike[Any]) -> UNIXSocketStream:
+ """
+ Connect to the given UNIX socket.
+
+ Not available on Windows.
+
+ :param path: path to the socket
+ :return: a socket stream object
+ :raises ConnectionFailed: if the connection fails
+
+ """
+ path = os.fspath(path)
+ return await get_async_backend().connect_unix(path)
+
+
+async def create_tcp_listener(
+ *,
+ local_host: IPAddressType | None = None,
+ local_port: int = 0,
+ family: AnyIPAddressFamily = socket.AddressFamily.AF_UNSPEC,
+ backlog: int = 65536,
+ reuse_port: bool = False,
+) -> MultiListener[SocketStream]:
+ """
+ Create a TCP socket listener.
+
+ :param local_port: port number to listen on
+ :param local_host: IP address of the interface to listen on. If omitted, listen on
+ all IPv4 and IPv6 interfaces. To listen on all interfaces on a specific address
+ family, use ``0.0.0.0`` for IPv4 or ``::`` for IPv6.
+ :param family: address family (used if ``local_host`` was omitted)
+ :param backlog: maximum number of queued incoming connections (up to a maximum of
+ 2**16, or 65536)
+ :param reuse_port: ``True`` to allow multiple sockets to bind to the same
+ address/port (not supported on Windows)
+ :return: a multi-listener object containing one or more socket listeners
+ :raises OSError: if there's an error creating a socket, or binding to one or more
+ interfaces failed
+
+ """
+ asynclib = get_async_backend()
+ backlog = min(backlog, 65536)
+ local_host = str(local_host) if local_host is not None else None
+
+ def setup_raw_socket(
+ fam: AddressFamily,
+ bind_addr: tuple[str, int] | tuple[str, int, int, int],
+ *,
+ v6only: bool = True,
+ ) -> socket.socket:
+ sock = socket.socket(fam)
+ try:
+ sock.setblocking(False)
+
+ if fam == AddressFamily.AF_INET6:
+ sock.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, v6only)
+
+ # For Windows, enable exclusive address use. For others, enable address
+ # reuse.
+ if sys.platform == "win32":
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
+ else:
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ if reuse_port:
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+
+ # Workaround for #554
+ if fam == socket.AF_INET6 and "%" in bind_addr[0]:
+ addr, scope_id = bind_addr[0].split("%", 1)
+ bind_addr = (addr, bind_addr[1], 0, int(scope_id))
+
+ sock.bind(bind_addr)
+ sock.listen(backlog)
+ except BaseException:
+ sock.close()
+ raise
+
+ return sock
+
+ # We passing type=0 on non-Windows platforms as a workaround for a uvloop bug
+ # where we don't get the correct scope ID for IPv6 link-local addresses when passing
+ # type=socket.SOCK_STREAM to getaddrinfo():
+ # https://github.com/MagicStack/uvloop/issues/539
+ gai_res = await getaddrinfo(
+ local_host,
+ local_port,
+ family=family,
+ type=socket.SOCK_STREAM if sys.platform == "win32" else 0,
+ flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG,
+ )
+
+ # The set comprehension is here to work around a glibc bug:
+ # https://sourceware.org/bugzilla/show_bug.cgi?id=14969
+ sockaddrs = sorted({res for res in gai_res if res[1] == SocketKind.SOCK_STREAM})
+
+ # Special case for dual-stack binding on the "any" interface
+ if (
+ local_host is None
+ and family == AddressFamily.AF_UNSPEC
+ and socket.has_dualstack_ipv6()
+ and any(fam == AddressFamily.AF_INET6 for fam, *_ in gai_res)
+ ):
+ raw_socket = setup_raw_socket(
+ AddressFamily.AF_INET6, ("::", local_port), v6only=False
+ )
+ listener = asynclib.create_tcp_listener(raw_socket)
+ return MultiListener([listener])
+
+ errors: list[OSError] = []
+ try:
+ for _ in range(len(sockaddrs)):
+ listeners: list[SocketListener] = []
+ bound_ephemeral_port = local_port
+ try:
+ for fam, *_, sockaddr in sockaddrs:
+ sockaddr = sockaddr[0], bound_ephemeral_port, *sockaddr[2:]
+ raw_socket = setup_raw_socket(fam, sockaddr)
+
+ # Store the assigned port if an ephemeral port was requested, so
+ # we'll bind to the same port on all interfaces
+ if local_port == 0 and len(gai_res) > 1:
+ bound_ephemeral_port = raw_socket.getsockname()[1]
+
+ listeners.append(asynclib.create_tcp_listener(raw_socket))
+ except BaseException as exc:
+ for listener in listeners:
+ await listener.aclose()
+
+ # If an ephemeral port was requested but binding the assigned port
+ # failed for another interface, rotate the address list and try again
+ if (
+ isinstance(exc, OSError)
+ and exc.errno == errno.EADDRINUSE
+ and local_port == 0
+ and bound_ephemeral_port
+ ):
+ errors.append(exc)
+ sockaddrs.append(sockaddrs.pop(0))
+ continue
+
+ raise
+
+ return MultiListener(listeners)
+
+ raise OSError(
+ f"Could not create {len(sockaddrs)} listeners with a consistent port"
+ ) from ExceptionGroup("Several bind attempts failed", errors)
+ finally:
+ del errors # Prevent reference cycles
+
+
+async def create_unix_listener(
+ path: str | bytes | PathLike[Any],
+ *,
+ mode: int | None = None,
+ backlog: int = 65536,
+) -> SocketListener:
+ """
+ Create a UNIX socket listener.
+
+ Not available on Windows.
+
+ :param path: path of the socket
+ :param mode: permissions to set on the socket
+ :param backlog: maximum number of queued incoming connections (up to a maximum of
+ 2**16, or 65536)
+ :return: a listener object
+
+ .. versionchanged:: 3.0
+ If a socket already exists on the file system in the given path, it will be
+ removed first.
+
+ """
+ backlog = min(backlog, 65536)
+ raw_socket = await setup_unix_local_socket(path, mode, socket.SOCK_STREAM)
+ try:
+ raw_socket.listen(backlog)
+ return get_async_backend().create_unix_listener(raw_socket)
+ except BaseException:
+ raw_socket.close()
+ raise
+
+
+async def create_udp_socket(
+ family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC,
+ *,
+ local_host: IPAddressType | None = None,
+ local_port: int = 0,
+ reuse_port: bool = False,
+) -> UDPSocket:
+ """
+ Create a UDP socket.
+
+ If ``port`` has been given, the socket will be bound to this port on the local
+ machine, making this socket suitable for providing UDP based services.
+
+ :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically
+ determined from ``local_host`` if omitted
+ :param local_host: IP address or host name of the local interface to bind to
+ :param local_port: local port to bind to
+ :param reuse_port: ``True`` to allow multiple sockets to bind to the same
+ address/port (not supported on Windows)
+ :return: a UDP socket
+
+ """
+ if family is AddressFamily.AF_UNSPEC and not local_host:
+ raise ValueError('Either "family" or "local_host" must be given')
+
+ if local_host:
+ gai_res = await getaddrinfo(
+ str(local_host),
+ local_port,
+ family=family,
+ type=socket.SOCK_DGRAM,
+ flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG,
+ )
+ family = cast(AnyIPAddressFamily, gai_res[0][0])
+ local_address = gai_res[0][-1]
+ elif family is AddressFamily.AF_INET6:
+ local_address = ("::", 0)
+ else:
+ local_address = ("0.0.0.0", 0)
+
+ sock = await get_async_backend().create_udp_socket(
+ family, local_address, None, reuse_port
+ )
+ return cast(UDPSocket, sock)
+
+
+async def create_connected_udp_socket(
+ remote_host: IPAddressType,
+ remote_port: int,
+ *,
+ family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC,
+ local_host: IPAddressType | None = None,
+ local_port: int = 0,
+ reuse_port: bool = False,
+) -> ConnectedUDPSocket:
+ """
+ Create a connected UDP socket.
+
+ Connected UDP sockets can only communicate with the specified remote host/port, an
+ any packets sent from other sources are dropped.
+
+ :param remote_host: remote host to set as the default target
+ :param remote_port: port on the remote host to set as the default target
+ :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically
+ determined from ``local_host`` or ``remote_host`` if omitted
+ :param local_host: IP address or host name of the local interface to bind to
+ :param local_port: local port to bind to
+ :param reuse_port: ``True`` to allow multiple sockets to bind to the same
+ address/port (not supported on Windows)
+ :return: a connected UDP socket
+
+ """
+ local_address = None
+ if local_host:
+ gai_res = await getaddrinfo(
+ str(local_host),
+ local_port,
+ family=family,
+ type=socket.SOCK_DGRAM,
+ flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG,
+ )
+ family = cast(AnyIPAddressFamily, gai_res[0][0])
+ local_address = gai_res[0][-1]
+
+ gai_res = await getaddrinfo(
+ str(remote_host), remote_port, family=family, type=socket.SOCK_DGRAM
+ )
+ family = cast(AnyIPAddressFamily, gai_res[0][0])
+ remote_address = gai_res[0][-1]
+
+ sock = await get_async_backend().create_udp_socket(
+ family, local_address, remote_address, reuse_port
+ )
+ return cast(ConnectedUDPSocket, sock)
+
+
+async def create_unix_datagram_socket(
+ *,
+ local_path: None | str | bytes | PathLike[Any] = None,
+ local_mode: int | None = None,
+) -> UNIXDatagramSocket:
+ """
+ Create a UNIX datagram socket.
+
+ Not available on Windows.
+
+ If ``local_path`` has been given, the socket will be bound to this path, making this
+ socket suitable for receiving datagrams from other processes. Other processes can
+ send datagrams to this socket only if ``local_path`` is set.
+
+ If a socket already exists on the file system in the ``local_path``, it will be
+ removed first.
+
+ :param local_path: the path on which to bind to
+ :param local_mode: permissions to set on the local socket
+ :return: a UNIX datagram socket
+
+ """
+ raw_socket = await setup_unix_local_socket(
+ local_path, local_mode, socket.SOCK_DGRAM
+ )
+ return await get_async_backend().create_unix_datagram_socket(raw_socket, None)
+
+
+async def create_connected_unix_datagram_socket(
+ remote_path: str | bytes | PathLike[Any],
+ *,
+ local_path: None | str | bytes | PathLike[Any] = None,
+ local_mode: int | None = None,
+) -> ConnectedUNIXDatagramSocket:
+ """
+ Create a connected UNIX datagram socket.
+
+ Connected datagram sockets can only communicate with the specified remote path.
+
+ If ``local_path`` has been given, the socket will be bound to this path, making
+ this socket suitable for receiving datagrams from other processes. Other processes
+ can send datagrams to this socket only if ``local_path`` is set.
+
+ If a socket already exists on the file system in the ``local_path``, it will be
+ removed first.
+
+ :param remote_path: the path to set as the default target
+ :param local_path: the path on which to bind to
+ :param local_mode: permissions to set on the local socket
+ :return: a connected UNIX datagram socket
+
+ """
+ remote_path = os.fspath(remote_path)
+ raw_socket = await setup_unix_local_socket(
+ local_path, local_mode, socket.SOCK_DGRAM
+ )
+ return await get_async_backend().create_unix_datagram_socket(
+ raw_socket, remote_path
+ )
+
+
+async def getaddrinfo(
+ host: bytes | str | None,
+ port: str | int | None,
+ *,
+ family: int | AddressFamily = 0,
+ type: int | SocketKind = 0,
+ proto: int = 0,
+ flags: int = 0,
+) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int]]]:
+ """
+ Look up a numeric IP address given a host name.
+
+ Internationalized domain names are translated according to the (non-transitional)
+ IDNA 2008 standard.
+
+ .. note:: 4-tuple IPv6 socket addresses are automatically converted to 2-tuples of
+ (host, port), unlike what :func:`socket.getaddrinfo` does.
+
+ :param host: host name
+ :param port: port number
+ :param family: socket family (`'AF_INET``, ...)
+ :param type: socket type (``SOCK_STREAM``, ...)
+ :param proto: protocol number
+ :param flags: flags to pass to upstream ``getaddrinfo()``
+ :return: list of tuples containing (family, type, proto, canonname, sockaddr)
+
+ .. seealso:: :func:`socket.getaddrinfo`
+
+ """
+ # Handle unicode hostnames
+ encoded_host = idna2008_resolve(host) if isinstance(host, str) else host
+ gai_res = await get_async_backend().getaddrinfo(
+ encoded_host, port, family=family, type=type, proto=proto, flags=flags
+ )
+ return [
+ (family, type, proto, canonname, convert_ipv6_sockaddr(sockaddr))
+ for family, type, proto, canonname, sockaddr in gai_res
+ # filter out IPv6 results when IPv6 is disabled
+ if not isinstance(sockaddr[0], int)
+ ]
+
+
+def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str, str]]:
+ """
+ Look up the host name of an IP address.
+
+ :param sockaddr: socket address (e.g. (ipaddress, port) for IPv4)
+ :param flags: flags to pass to upstream ``getnameinfo()``
+ :return: a tuple of (host name, service name)
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ .. seealso:: :func:`socket.getnameinfo`
+
+ """
+ return get_async_backend().getnameinfo(sockaddr, flags)
+
+
+@deprecated("This function is deprecated; use `wait_readable` instead")
+def wait_socket_readable(sock: socket.socket) -> Awaitable[None]:
+ """
+ .. deprecated:: 4.7.0
+ Use :func:`wait_readable` instead.
+
+ Wait until the given socket has data to be read.
+
+ .. warning:: Only use this on raw sockets that have not been wrapped by any higher
+ level constructs like socket streams!
+
+ :param sock: a socket object
+ :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the
+ socket to become readable
+ :raises ~anyio.BusyResourceError: if another task is already waiting for the socket
+ to become readable
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().wait_readable(sock.fileno())
+
+
+@deprecated("This function is deprecated; use `wait_writable` instead")
+def wait_socket_writable(sock: socket.socket) -> Awaitable[None]:
+ """
+ .. deprecated:: 4.7.0
+ Use :func:`wait_writable` instead.
+
+ Wait until the given socket can be written to.
+
+ This does **NOT** work on Windows when using the asyncio backend with a proactor
+ event loop (default on py3.8+).
+
+ .. warning:: Only use this on raw sockets that have not been wrapped by any higher
+ level constructs like socket streams!
+
+ :param sock: a socket object
+ :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the
+ socket to become writable
+ :raises ~anyio.BusyResourceError: if another task is already waiting for the socket
+ to become writable
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().wait_writable(sock.fileno())
+
+
+def wait_readable(obj: FileDescriptorLike) -> Awaitable[None]:
+ """
+ Wait until the given object has data to be read.
+
+ On Unix systems, ``obj`` must either be an integer file descriptor, or else an
+ object with a ``.fileno()`` method which returns an integer file descriptor. Any
+ kind of file descriptor can be passed, though the exact semantics will depend on
+ your kernel. For example, this probably won't do anything useful for on-disk files.
+
+ On Windows systems, ``obj`` must either be an integer ``SOCKET`` handle, or else an
+ object with a ``.fileno()`` method which returns an integer ``SOCKET`` handle. File
+ descriptors aren't supported, and neither are handles that refer to anything besides
+ a ``SOCKET``.
+
+ On backends where this functionality is not natively provided (asyncio
+ ``ProactorEventLoop`` on Windows), it is provided using a separate selector thread
+ which is set to shut down when the interpreter shuts down.
+
+ .. warning:: Don't use this on raw sockets that have been wrapped by any higher
+ level constructs like socket streams!
+
+ :param obj: an object with a ``.fileno()`` method or an integer handle
+ :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the
+ object to become readable
+ :raises ~anyio.BusyResourceError: if another task is already waiting for the object
+ to become readable
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().wait_readable(obj)
+
+
+def wait_writable(obj: FileDescriptorLike) -> Awaitable[None]:
+ """
+ Wait until the given object can be written to.
+
+ :param obj: an object with a ``.fileno()`` method or an integer handle
+ :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the
+ object to become writable
+ :raises ~anyio.BusyResourceError: if another task is already waiting for the object
+ to become writable
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ .. seealso:: See the documentation of :func:`wait_readable` for the definition of
+ ``obj`` and notes on backend compatibility.
+
+ .. warning:: Don't use this on raw sockets that have been wrapped by any higher
+ level constructs like socket streams!
+
+ """
+ return get_async_backend().wait_writable(obj)
+
+
+def notify_closing(obj: FileDescriptorLike) -> None:
+ """
+ Call this before closing a file descriptor (on Unix) or socket (on
+ Windows). This will cause any `wait_readable` or `wait_writable`
+ calls on the given object to immediately wake up and raise
+ `~anyio.ClosedResourceError`.
+
+ This doesn't actually close the object – you still have to do that
+ yourself afterwards. Also, you want to be careful to make sure no
+ new tasks start waiting on the object in between when you call this
+ and when it's actually closed. So to close something properly, you
+ usually want to do these steps in order:
+
+ 1. Explicitly mark the object as closed, so that any new attempts
+ to use it will abort before they start.
+ 2. Call `notify_closing` to wake up any already-existing users.
+ 3. Actually close the object.
+
+ It's also possible to do them in a different order if that's more
+ convenient, *but only if* you make sure not to have any checkpoints in
+ between the steps. This way they all happen in a single atomic
+ step, so other tasks won't be able to tell what order they happened
+ in anyway.
+
+ :param obj: an object with a ``.fileno()`` method or an integer handle
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ get_async_backend().notify_closing(obj)
+
+
+#
+# Private API
+#
+
+
+def convert_ipv6_sockaddr(
+ sockaddr: tuple[str, int, int, int] | tuple[str, int],
+) -> tuple[str, int]:
+ """
+ Convert a 4-tuple IPv6 socket address to a 2-tuple (address, port) format.
+
+ If the scope ID is nonzero, it is added to the address, separated with ``%``.
+ Otherwise the flow id and scope id are simply cut off from the tuple.
+ Any other kinds of socket addresses are returned as-is.
+
+ :param sockaddr: the result of :meth:`~socket.socket.getsockname`
+ :return: the converted socket address
+
+ """
+ # This is more complicated than it should be because of MyPy
+ if isinstance(sockaddr, tuple) and len(sockaddr) == 4:
+ host, port, _flowinfo, scope_id = sockaddr
+ if scope_id:
+ # PyPy (as of v7.3.11) leaves the interface name in the result, so
+ # we discard it and only get the scope ID from the end
+ # (https://foss.heptapod.net/pypy/pypy/-/issues/3938)
+ host = host.split("%")[0]
+
+ # Add scope_id to the address
+ return f"{host}%{scope_id}", port
+ else:
+ return host, port
+ else:
+ return sockaddr
+
+
+async def setup_unix_local_socket(
+ path: None | str | bytes | PathLike[Any],
+ mode: int | None,
+ socktype: int,
+) -> socket.socket:
+ """
+ Create a UNIX local socket object, deleting the socket at the given path if it
+ exists.
+
+ Not available on Windows.
+
+ :param path: path of the socket
+ :param mode: permissions to set on the socket
+ :param socktype: socket.SOCK_STREAM or socket.SOCK_DGRAM
+
+ """
+ path_str: str | None
+ if path is not None:
+ path_str = os.fsdecode(path)
+
+ # Linux abstract namespace sockets aren't backed by a concrete file so skip stat call
+ if not path_str.startswith("\0"):
+ # Copied from pathlib...
+ try:
+ stat_result = os.stat(path)
+ except OSError as e:
+ if e.errno not in (
+ errno.ENOENT,
+ errno.ENOTDIR,
+ errno.EBADF,
+ errno.ELOOP,
+ ):
+ raise
+ else:
+ if stat.S_ISSOCK(stat_result.st_mode):
+ os.unlink(path)
+ else:
+ path_str = None
+
+ raw_socket = socket.socket(socket.AF_UNIX, socktype)
+ raw_socket.setblocking(False)
+
+ if path_str is not None:
+ try:
+ await to_thread.run_sync(raw_socket.bind, path_str, abandon_on_cancel=True)
+ if mode is not None:
+ await to_thread.run_sync(chmod, path_str, mode, abandon_on_cancel=True)
+ except BaseException:
+ raw_socket.close()
+ raise
+
+ return raw_socket
+
+
+@dataclass
+class TCPConnectable(ByteStreamConnectable):
+ """
+ Connects to a TCP server at the given host and port.
+
+ :param host: host name or IP address of the server
+ :param port: TCP port number of the server
+ """
+
+ host: str | IPv4Address | IPv6Address
+ port: int
+
+ def __post_init__(self) -> None:
+ if self.port < 1 or self.port > 65535:
+ raise ValueError("TCP port number out of range")
+
+ @override
+ async def connect(self) -> SocketStream:
+ try:
+ return await connect_tcp(self.host, self.port)
+ except OSError as exc:
+ raise ConnectionFailed(
+ f"error connecting to {self.host}:{self.port}: {exc}"
+ ) from exc
+
+
+@dataclass
+class UNIXConnectable(ByteStreamConnectable):
+ """
+ Connects to a UNIX domain socket at the given path.
+
+ :param path: the file system path of the socket
+ """
+
+ path: str | bytes | PathLike[str] | PathLike[bytes]
+
+ @override
+ async def connect(self) -> UNIXSocketStream:
+ try:
+ return await connect_unix(self.path)
+ except OSError as exc:
+ raise ConnectionFailed(f"error connecting to {self.path!r}: {exc}") from exc
+
+
+def as_connectable(
+ remote: ByteStreamConnectable
+ | tuple[str | IPv4Address | IPv6Address, int]
+ | str
+ | bytes
+ | PathLike[str],
+ /,
+ *,
+ tls: bool = False,
+ ssl_context: ssl.SSLContext | None = None,
+ tls_hostname: str | None = None,
+ tls_standard_compatible: bool = True,
+) -> ByteStreamConnectable:
+ """
+ Return a byte stream connectable from the given object.
+
+ If a bytestream connectable is given, it is returned unchanged.
+ If a tuple of (host, port) is given, a TCP connectable is returned.
+ If a string or bytes path is given, a UNIX connectable is returned.
+
+ If ``tls=True``, the connectable will be wrapped in a
+ :class:`~.streams.tls.TLSConnectable`.
+
+ :param remote: a connectable, a tuple of (host, port) or a path to a UNIX socket
+ :param tls: if ``True``, wrap the plaintext connectable in a
+ :class:`~.streams.tls.TLSConnectable`, using the provided TLS settings)
+ :param ssl_context: if ``tls=True``, the SSLContext object to use (if not provided,
+ a secure default will be created)
+ :param tls_hostname: if ``tls=True``, host name of the server to use for checking
+ the server certificate (defaults to the host portion of the address for TCP
+ connectables)
+ :param tls_standard_compatible: if ``False`` and ``tls=True``, makes the TLS stream
+ skip the closing handshake when closing the connection, so it won't raise an
+ exception if the server does the same
+
+ """
+ connectable: TCPConnectable | UNIXConnectable | TLSConnectable
+ if isinstance(remote, ByteStreamConnectable):
+ return remote
+ elif isinstance(remote, tuple) and len(remote) == 2:
+ connectable = TCPConnectable(*remote)
+ elif isinstance(remote, (str, bytes, PathLike)):
+ connectable = UNIXConnectable(remote)
+ else:
+ raise TypeError(f"cannot convert {remote!r} to a connectable")
+
+ if tls:
+ if not tls_hostname and isinstance(connectable, TCPConnectable):
+ tls_hostname = str(connectable.host)
+
+ connectable = TLSConnectable(
+ connectable,
+ ssl_context=ssl_context,
+ hostname=tls_hostname,
+ standard_compatible=tls_standard_compatible,
+ )
+
+ return connectable
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_streams.py b/venv/lib/python3.11/site-packages/anyio/_core/_streams.py
new file mode 100644
index 000000000..2b9c7df20
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_streams.py
@@ -0,0 +1,52 @@
+from __future__ import annotations
+
+import math
+from typing import TypeVar
+from warnings import warn
+
+from ..streams.memory import (
+ MemoryObjectReceiveStream,
+ MemoryObjectSendStream,
+ _MemoryObjectStreamState,
+)
+
+T_Item = TypeVar("T_Item")
+
+
+class create_memory_object_stream(
+ tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]],
+):
+ """
+ Create a memory object stream.
+
+ The stream's item type can be annotated like
+ :func:`create_memory_object_stream[T_Item]`.
+
+ :param max_buffer_size: number of items held in the buffer until ``send()`` starts
+ blocking
+ :param item_type: old way of marking the streams with the right generic type for
+ static typing (does nothing on AnyIO 4)
+
+ .. deprecated:: 4.0
+ Use ``create_memory_object_stream[YourItemType](...)`` instead.
+ :return: a tuple of (send stream, receive stream)
+
+ """
+
+ def __new__( # type: ignore[misc]
+ cls, max_buffer_size: float = 0, item_type: object = None
+ ) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]:
+ if max_buffer_size != math.inf and not isinstance(max_buffer_size, int):
+ raise ValueError("max_buffer_size must be either an integer or math.inf")
+ if max_buffer_size < 0:
+ raise ValueError("max_buffer_size cannot be negative")
+ if item_type is not None:
+ warn(
+ "The item_type argument has been deprecated in AnyIO 4.0. "
+ "Use create_memory_object_stream[YourItemType](...) instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ state = _MemoryObjectStreamState[T_Item](max_buffer_size)
+ return (MemoryObjectSendStream(state), MemoryObjectReceiveStream(state))
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py b/venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py
new file mode 100644
index 000000000..53184a2e6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py
@@ -0,0 +1,196 @@
+from __future__ import annotations
+
+from collections.abc import AsyncIterable, Iterable, Mapping, Sequence
+from io import BytesIO
+from os import PathLike
+from subprocess import PIPE, CalledProcessError, CompletedProcess
+from typing import IO, Any, TypeAlias, cast
+
+from ..abc import Process # noqa: TC001
+from ._eventloop import get_async_backend
+from ._tasks import create_task_group
+
+StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]
+
+
+async def run_process(
+ command: StrOrBytesPath | Sequence[StrOrBytesPath],
+ *,
+ input: bytes | None = None,
+ stdin: int | IO[Any] | None = None,
+ stdout: int | IO[Any] | None = PIPE,
+ stderr: int | IO[Any] | None = PIPE,
+ check: bool = True,
+ cwd: StrOrBytesPath | None = None,
+ env: Mapping[str, str] | None = None,
+ startupinfo: Any = None,
+ creationflags: int = 0,
+ start_new_session: bool = False,
+ pass_fds: Sequence[int] = (),
+ user: str | int | None = None,
+ group: str | int | None = None,
+ extra_groups: Iterable[str | int] | None = None,
+ umask: int = -1,
+) -> CompletedProcess[bytes]:
+ """
+ Run an external command in a subprocess and wait until it completes.
+
+ .. seealso:: :func:`subprocess.run`
+
+ :param command: either a string to pass to the shell, or an iterable of strings
+ containing the executable name or path and its arguments
+ :param input: bytes passed to the standard input of the subprocess
+ :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
+ a file-like object, or `None`; ``input`` overrides this
+ :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
+ a file-like object, or `None`
+ :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
+ :data:`subprocess.STDOUT`, a file-like object, or `None`
+ :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the
+ process terminates with a return code other than 0
+ :param cwd: If not ``None``, change the working directory to this before running the
+ command
+ :param env: if not ``None``, this mapping replaces the inherited environment
+ variables from the parent process
+ :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used
+ to specify process startup parameters (Windows only)
+ :param creationflags: flags that can be used to control the creation of the
+ subprocess (see :class:`subprocess.Popen` for the specifics)
+ :param start_new_session: if ``true`` the setsid() system call will be made in the
+ child process prior to the execution of the subprocess. (POSIX only)
+ :param pass_fds: sequence of file descriptors to keep open between the parent and
+ child processes. (POSIX only)
+ :param user: effective user to run the process as (Python >= 3.9, POSIX only)
+ :param group: effective group to run the process as (Python >= 3.9, POSIX only)
+ :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9,
+ POSIX only)
+ :param umask: if not negative, this umask is applied in the child process before
+ running the given command (Python >= 3.9, POSIX only)
+ :return: an object representing the completed process
+ :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process
+ exits with a nonzero return code
+
+ """
+
+ async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None:
+ buffer = BytesIO()
+ async for chunk in stream:
+ buffer.write(chunk)
+
+ stream_contents[index] = buffer.getvalue()
+
+ if stdin is not None and input is not None:
+ raise ValueError("only one of stdin and input is allowed")
+
+ async with await open_process(
+ command,
+ stdin=PIPE if input else stdin,
+ stdout=stdout,
+ stderr=stderr,
+ cwd=cwd,
+ env=env,
+ startupinfo=startupinfo,
+ creationflags=creationflags,
+ start_new_session=start_new_session,
+ pass_fds=pass_fds,
+ user=user,
+ group=group,
+ extra_groups=extra_groups,
+ umask=umask,
+ ) as process:
+ stream_contents: list[bytes | None] = [None, None]
+ async with create_task_group() as tg:
+ if process.stdout:
+ tg.start_soon(drain_stream, process.stdout, 0)
+
+ if process.stderr:
+ tg.start_soon(drain_stream, process.stderr, 1)
+
+ if process.stdin and input:
+ await process.stdin.send(input)
+ await process.stdin.aclose()
+
+ await process.wait()
+
+ output, errors = stream_contents
+ if check and process.returncode != 0:
+ raise CalledProcessError(cast(int, process.returncode), command, output, errors)
+
+ return CompletedProcess(command, cast(int, process.returncode), output, errors)
+
+
+async def open_process(
+ command: StrOrBytesPath | Sequence[StrOrBytesPath],
+ *,
+ stdin: int | IO[Any] | None = PIPE,
+ stdout: int | IO[Any] | None = PIPE,
+ stderr: int | IO[Any] | None = PIPE,
+ cwd: StrOrBytesPath | None = None,
+ env: Mapping[str, str] | None = None,
+ startupinfo: Any = None,
+ creationflags: int = 0,
+ start_new_session: bool = False,
+ pass_fds: Sequence[int] = (),
+ user: str | int | None = None,
+ group: str | int | None = None,
+ extra_groups: Iterable[str | int] | None = None,
+ umask: int = -1,
+) -> Process:
+ """
+ Start an external command in a subprocess.
+
+ .. seealso:: :class:`subprocess.Popen`
+
+ :param command: either a string to pass to the shell, or an iterable of strings
+ containing the executable name or path and its arguments
+ :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a
+ file-like object, or ``None``
+ :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
+ a file-like object, or ``None``
+ :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`,
+ :data:`subprocess.STDOUT`, a file-like object, or ``None``
+ :param cwd: If not ``None``, the working directory is changed before executing
+ :param env: If env is not ``None``, it must be a mapping that defines the
+ environment variables for the new process
+ :param creationflags: flags that can be used to control the creation of the
+ subprocess (see :class:`subprocess.Popen` for the specifics)
+ :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used
+ to specify process startup parameters (Windows only)
+ :param start_new_session: if ``true`` the setsid() system call will be made in the
+ child process prior to the execution of the subprocess. (POSIX only)
+ :param pass_fds: sequence of file descriptors to keep open between the parent and
+ child processes. (POSIX only)
+ :param user: effective user to run the process as (POSIX only)
+ :param group: effective group to run the process as (POSIX only)
+ :param extra_groups: supplementary groups to set in the subprocess (POSIX only)
+ :param umask: if not negative, this umask is applied in the child process before
+ running the given command (POSIX only)
+ :return: an asynchronous process object
+
+ """
+ kwargs: dict[str, Any] = {}
+ if user is not None:
+ kwargs["user"] = user
+
+ if group is not None:
+ kwargs["group"] = group
+
+ if extra_groups is not None:
+ kwargs["extra_groups"] = extra_groups
+
+ if umask >= 0:
+ kwargs["umask"] = umask
+
+ return await get_async_backend().open_process(
+ command,
+ stdin=stdin,
+ stdout=stdout,
+ stderr=stderr,
+ cwd=cwd,
+ env=env,
+ startupinfo=startupinfo,
+ creationflags=creationflags,
+ start_new_session=start_new_session,
+ pass_fds=pass_fds,
+ **kwargs,
+ )
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_synchronization.py b/venv/lib/python3.11/site-packages/anyio/_core/_synchronization.py
new file mode 100644
index 000000000..cb988c60d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_synchronization.py
@@ -0,0 +1,778 @@
+from __future__ import annotations
+
+import math
+import sys
+from collections import deque
+from collections.abc import Callable
+from dataclasses import dataclass
+from types import TracebackType
+from typing import TypeVar
+
+from ..lowlevel import checkpoint_if_cancelled
+from ._eventloop import get_async_backend
+from ._exceptions import BusyResourceError, NoEventLoopError
+from ._tasks import CancelScope
+from ._testing import TaskInfo, get_current_task
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
+
+T = TypeVar("T")
+
+
+@dataclass(frozen=True)
+class EventStatistics:
+ """
+ :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait`
+ """
+
+ tasks_waiting: int
+
+
+@dataclass(frozen=True)
+class CapacityLimiterStatistics:
+ """
+ :ivar int borrowed_tokens: number of tokens currently borrowed by tasks
+ :ivar float total_tokens: total number of available tokens
+ :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from
+ this limiter
+ :ivar int tasks_waiting: number of tasks waiting on
+ :meth:`~.CapacityLimiter.acquire` or
+ :meth:`~.CapacityLimiter.acquire_on_behalf_of`
+ """
+
+ borrowed_tokens: int
+ total_tokens: float
+ borrowers: tuple[object, ...]
+ tasks_waiting: int
+
+
+@dataclass(frozen=True)
+class LockStatistics:
+ """
+ :ivar bool locked: flag indicating if this lock is locked or not
+ :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the
+ lock is not held by any task)
+ :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire`
+ """
+
+ locked: bool
+ owner: TaskInfo | None
+ tasks_waiting: int
+
+
+@dataclass(frozen=True)
+class ConditionStatistics:
+ """
+ :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait`
+ :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying
+ :class:`~.Lock`
+ """
+
+ tasks_waiting: int
+ lock_statistics: LockStatistics
+
+
+@dataclass(frozen=True)
+class SemaphoreStatistics:
+ """
+ :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire`
+
+ """
+
+ tasks_waiting: int
+
+
+class Event:
+ __slots__ = ("__weakref__",)
+
+ def __new__(cls) -> Event:
+ try:
+ return get_async_backend().create_event()
+ except NoEventLoopError:
+ return EventAdapter()
+
+ def set(self) -> None:
+ """Set the flag, notifying all listeners."""
+ raise NotImplementedError
+
+ def is_set(self) -> bool:
+ """Return ``True`` if the flag is set, ``False`` if not."""
+ raise NotImplementedError
+
+ async def wait(self) -> None:
+ """
+ Wait until the flag has been set.
+
+ If the flag has already been set when this method is called, it returns
+ immediately.
+
+ """
+ raise NotImplementedError
+
+ def statistics(self) -> EventStatistics:
+ """Return statistics about the current state of this event."""
+ raise NotImplementedError
+
+
+class EventAdapter(Event):
+ __slots__ = "_internal_event", "_is_set"
+
+ def __new__(cls) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self) -> None:
+ self._internal_event: Event | None = None
+ self._is_set = False
+
+ @property
+ def _event(self) -> Event:
+ if self._internal_event is None:
+ self._internal_event = get_async_backend().create_event()
+ if self._is_set:
+ self._internal_event.set()
+
+ return self._internal_event
+
+ def set(self) -> None:
+ if self._internal_event is None:
+ self._is_set = True
+ else:
+ self._event.set()
+
+ def is_set(self) -> bool:
+ if self._internal_event is None:
+ return self._is_set
+
+ return self._internal_event.is_set()
+
+ async def wait(self) -> None:
+ await self._event.wait()
+
+ def statistics(self) -> EventStatistics:
+ if self._internal_event is None:
+ return EventStatistics(tasks_waiting=0)
+
+ return self._internal_event.statistics()
+
+
+class Lock:
+ __slots__ = ("__weakref__",)
+
+ def __new__(cls, *, fast_acquire: bool = False) -> Lock:
+ try:
+ return get_async_backend().create_lock(fast_acquire=fast_acquire)
+ except NoEventLoopError:
+ return LockAdapter(fast_acquire=fast_acquire)
+
+ async def __aenter__(self) -> None:
+ await self.acquire()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.release()
+
+ async def acquire(self) -> None:
+ """Acquire the lock."""
+ raise NotImplementedError
+
+ def acquire_nowait(self) -> None:
+ """
+ Acquire the lock, without blocking.
+
+ :raises ~anyio.WouldBlock: if the operation would block
+
+ """
+ raise NotImplementedError
+
+ def release(self) -> None:
+ """Release the lock."""
+ raise NotImplementedError
+
+ def locked(self) -> bool:
+ """Return True if the lock is currently held."""
+ raise NotImplementedError
+
+ def statistics(self) -> LockStatistics:
+ """
+ Return statistics about the current state of this lock.
+
+ .. versionadded:: 3.0
+ """
+ raise NotImplementedError
+
+
+class LockAdapter(Lock):
+ __slots__ = "_fast_acquire", "_internal_lock"
+
+ def __new__(cls, *, fast_acquire: bool = False) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self, *, fast_acquire: bool = False):
+ self._internal_lock: Lock | None = None
+ self._fast_acquire = fast_acquire
+
+ @property
+ def _lock(self) -> Lock:
+ if self._internal_lock is None:
+ self._internal_lock = get_async_backend().create_lock(
+ fast_acquire=self._fast_acquire
+ )
+
+ return self._internal_lock
+
+ async def __aenter__(self) -> None:
+ await self._lock.acquire()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ if self._internal_lock is not None:
+ self._internal_lock.release()
+
+ async def acquire(self) -> None:
+ """Acquire the lock."""
+ await self._lock.acquire()
+
+ def acquire_nowait(self) -> None:
+ """
+ Acquire the lock, without blocking.
+
+ :raises ~anyio.WouldBlock: if the operation would block
+
+ """
+ self._lock.acquire_nowait()
+
+ def release(self) -> None:
+ """Release the lock."""
+ self._lock.release()
+
+ def locked(self) -> bool:
+ """Return True if the lock is currently held."""
+ return self._lock.locked()
+
+ def statistics(self) -> LockStatistics:
+ """
+ Return statistics about the current state of this lock.
+
+ .. versionadded:: 3.0
+
+ """
+ if self._internal_lock is None:
+ return LockStatistics(False, None, 0)
+
+ return self._internal_lock.statistics()
+
+
+class Condition:
+ __slots__ = "__weakref__", "_lock", "_owner_task", "_waiters"
+
+ def __init__(self, lock: Lock | None = None):
+ self._owner_task: TaskInfo | None = None
+ self._lock = lock or Lock()
+ self._waiters: deque[Event] = deque()
+
+ async def __aenter__(self) -> None:
+ await self.acquire()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.release()
+
+ def _check_acquired(self) -> None:
+ if self._owner_task != get_current_task():
+ raise RuntimeError("The current task is not holding the underlying lock")
+
+ async def acquire(self) -> None:
+ """Acquire the underlying lock."""
+ await self._lock.acquire()
+ self._owner_task = get_current_task()
+
+ def acquire_nowait(self) -> None:
+ """
+ Acquire the underlying lock, without blocking.
+
+ :raises ~anyio.WouldBlock: if the operation would block
+
+ """
+ self._lock.acquire_nowait()
+ self._owner_task = get_current_task()
+
+ def release(self) -> None:
+ """Release the underlying lock."""
+ self._lock.release()
+
+ def locked(self) -> bool:
+ """Return True if the lock is set."""
+ return self._lock.locked()
+
+ def notify(self, n: int = 1) -> None:
+ """Notify exactly n listeners."""
+ self._check_acquired()
+ for _ in range(n):
+ try:
+ event = self._waiters.popleft()
+ except IndexError:
+ break
+
+ event.set()
+
+ def notify_all(self) -> None:
+ """Notify all the listeners."""
+ self._check_acquired()
+ for event in self._waiters:
+ event.set()
+
+ self._waiters.clear()
+
+ async def wait(self) -> None:
+ """Wait for a notification."""
+ await checkpoint_if_cancelled()
+ self._check_acquired()
+ event = Event()
+ self._waiters.append(event)
+ self.release()
+ try:
+ await event.wait()
+ except BaseException:
+ if not event.is_set():
+ self._waiters.remove(event)
+ elif self._waiters:
+ # This task was notified by could not act on it, so pass
+ # it on to the next task
+ self._waiters.popleft().set()
+
+ raise
+ finally:
+ with CancelScope(shield=True):
+ await self.acquire()
+
+ async def wait_for(self, predicate: Callable[[], T]) -> T:
+ """
+ Wait until a predicate becomes true.
+
+ :param predicate: a callable that returns a truthy value when the condition is
+ met
+ :return: the result of the predicate
+
+ .. versionadded:: 4.11.0
+
+ """
+ while not (result := predicate()):
+ await self.wait()
+
+ return result
+
+ def statistics(self) -> ConditionStatistics:
+ """
+ Return statistics about the current state of this condition.
+
+ .. versionadded:: 3.0
+ """
+ return ConditionStatistics(len(self._waiters), self._lock.statistics())
+
+
+class Semaphore:
+ __slots__ = "__weakref__", "_fast_acquire"
+
+ def __new__(
+ cls,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> Semaphore:
+ try:
+ return get_async_backend().create_semaphore(
+ initial_value, max_value=max_value, fast_acquire=fast_acquire
+ )
+ except NoEventLoopError:
+ return SemaphoreAdapter(initial_value, max_value=max_value)
+
+ def __init__(
+ self,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ):
+ if not isinstance(initial_value, int):
+ raise TypeError("initial_value must be an integer")
+ if initial_value < 0:
+ raise ValueError("initial_value must be >= 0")
+ if max_value is not None:
+ if not isinstance(max_value, int):
+ raise TypeError("max_value must be an integer or None")
+ if max_value < initial_value:
+ raise ValueError(
+ "max_value must be equal to or higher than initial_value"
+ )
+
+ self._fast_acquire = fast_acquire
+
+ async def __aenter__(self) -> Self:
+ await self.acquire()
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.release()
+
+ async def acquire(self) -> None:
+ """Decrement the semaphore value, blocking if necessary."""
+ raise NotImplementedError
+
+ def acquire_nowait(self) -> None:
+ """
+ Acquire the underlying lock, without blocking.
+
+ :raises ~anyio.WouldBlock: if the operation would block
+
+ """
+ raise NotImplementedError
+
+ def release(self) -> None:
+ """Increment the semaphore value."""
+ raise NotImplementedError
+
+ @property
+ def value(self) -> int:
+ """The current value of the semaphore."""
+ raise NotImplementedError
+
+ @property
+ def max_value(self) -> int | None:
+ """The maximum value of the semaphore."""
+ raise NotImplementedError
+
+ def statistics(self) -> SemaphoreStatistics:
+ """
+ Return statistics about the current state of this semaphore.
+
+ .. versionadded:: 3.0
+ """
+ raise NotImplementedError
+
+
+class SemaphoreAdapter(Semaphore):
+ __slots__ = "_initial_value", "_internal_semaphore", "_max_value"
+
+ def __new__(
+ cls,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> Self:
+ return object.__new__(cls)
+
+ def __init__(
+ self,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> None:
+ super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire)
+ self._internal_semaphore: Semaphore | None = None
+ self._initial_value = initial_value
+ self._max_value = max_value
+
+ @property
+ def _semaphore(self) -> Semaphore:
+ if self._internal_semaphore is None:
+ self._internal_semaphore = get_async_backend().create_semaphore(
+ self._initial_value, max_value=self._max_value
+ )
+
+ return self._internal_semaphore
+
+ async def acquire(self) -> None:
+ await self._semaphore.acquire()
+
+ def acquire_nowait(self) -> None:
+ self._semaphore.acquire_nowait()
+
+ def release(self) -> None:
+ self._semaphore.release()
+
+ @property
+ def value(self) -> int:
+ if self._internal_semaphore is None:
+ return self._initial_value
+
+ return self._semaphore.value
+
+ @property
+ def max_value(self) -> int | None:
+ return self._max_value
+
+ def statistics(self) -> SemaphoreStatistics:
+ if self._internal_semaphore is None:
+ return SemaphoreStatistics(tasks_waiting=0)
+
+ return self._semaphore.statistics()
+
+
+class CapacityLimiter:
+ __slots__ = ("__weakref__",)
+
+ def __new__(cls, total_tokens: float) -> CapacityLimiter:
+ try:
+ return get_async_backend().create_capacity_limiter(total_tokens)
+ except NoEventLoopError:
+ return CapacityLimiterAdapter(total_tokens)
+
+ async def __aenter__(self) -> None:
+ raise NotImplementedError
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ raise NotImplementedError
+
+ @property
+ def total_tokens(self) -> float:
+ """
+ The total number of tokens available for borrowing.
+
+ This is a read-write property. If the total number of tokens is increased, the
+ proportionate number of tasks waiting on this limiter will be granted their
+ tokens.
+
+ .. versionchanged:: 3.0
+ The property is now writable.
+ .. versionchanged:: 4.12
+ The value can now be set to 0.
+
+ """
+ raise NotImplementedError
+
+ @total_tokens.setter
+ def total_tokens(self, value: float) -> None:
+ raise NotImplementedError
+
+ @property
+ def borrowed_tokens(self) -> int:
+ """The number of tokens that have currently been borrowed."""
+ raise NotImplementedError
+
+ @property
+ def available_tokens(self) -> float:
+ """The number of tokens currently available to be borrowed"""
+ raise NotImplementedError
+
+ def acquire_nowait(self) -> None:
+ """
+ Acquire a token for the current task without waiting for one to become
+ available.
+
+ :raises ~anyio.WouldBlock: if there are no tokens available for borrowing
+
+ """
+ raise NotImplementedError
+
+ def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
+ """
+ Acquire a token without waiting for one to become available.
+
+ :param borrower: the entity borrowing a token
+ :raises ~anyio.WouldBlock: if there are no tokens available for borrowing
+
+ """
+ raise NotImplementedError
+
+ async def acquire(self) -> None:
+ """
+ Acquire a token for the current task, waiting if necessary for one to become
+ available.
+
+ """
+ raise NotImplementedError
+
+ async def acquire_on_behalf_of(self, borrower: object) -> None:
+ """
+ Acquire a token, waiting if necessary for one to become available.
+
+ :param borrower: the entity borrowing a token
+
+ """
+ raise NotImplementedError
+
+ def release(self) -> None:
+ """
+ Release the token held by the current task.
+
+ :raises RuntimeError: if the current task has not borrowed a token from this
+ limiter.
+
+ """
+ raise NotImplementedError
+
+ def release_on_behalf_of(self, borrower: object) -> None:
+ """
+ Release the token held by the given borrower.
+
+ :raises RuntimeError: if the borrower has not borrowed a token from this
+ limiter.
+
+ """
+ raise NotImplementedError
+
+ def statistics(self) -> CapacityLimiterStatistics:
+ """
+ Return statistics about the current state of this limiter.
+
+ .. versionadded:: 3.0
+
+ """
+ raise NotImplementedError
+
+
+class CapacityLimiterAdapter(CapacityLimiter):
+ __slots__ = "_internal_limiter", "_total_tokens"
+
+ def __new__(cls, total_tokens: float) -> Self:
+ return object.__new__(cls)
+
+ def __init__(self, total_tokens: float) -> None:
+ self._internal_limiter: CapacityLimiter | None = None
+ self.total_tokens = total_tokens
+
+ @property
+ def _limiter(self) -> CapacityLimiter:
+ if self._internal_limiter is None:
+ self._internal_limiter = get_async_backend().create_capacity_limiter(
+ self._total_tokens
+ )
+
+ return self._internal_limiter
+
+ async def __aenter__(self) -> None:
+ await self._limiter.__aenter__()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ return await self._limiter.__aexit__(exc_type, exc_val, exc_tb)
+
+ @property
+ def total_tokens(self) -> float:
+ if self._internal_limiter is None:
+ return self._total_tokens
+
+ return self._internal_limiter.total_tokens
+
+ @total_tokens.setter
+ def total_tokens(self, value: float) -> None:
+ if not isinstance(value, int) and not math.isinf(value):
+ raise TypeError("total_tokens must be an int or math.inf")
+ elif value < 0:
+ raise ValueError("total_tokens must be >= 0")
+
+ if self._internal_limiter is None:
+ self._total_tokens = value
+ return
+
+ self._limiter.total_tokens = value
+
+ @property
+ def borrowed_tokens(self) -> int:
+ if self._internal_limiter is None:
+ return 0
+
+ return self._internal_limiter.borrowed_tokens
+
+ @property
+ def available_tokens(self) -> float:
+ if self._internal_limiter is None:
+ return self._total_tokens
+
+ return self._internal_limiter.available_tokens
+
+ def acquire_nowait(self) -> None:
+ self._limiter.acquire_nowait()
+
+ def acquire_on_behalf_of_nowait(self, borrower: object) -> None:
+ self._limiter.acquire_on_behalf_of_nowait(borrower)
+
+ async def acquire(self) -> None:
+ await self._limiter.acquire()
+
+ async def acquire_on_behalf_of(self, borrower: object) -> None:
+ await self._limiter.acquire_on_behalf_of(borrower)
+
+ def release(self) -> None:
+ self._limiter.release()
+
+ def release_on_behalf_of(self, borrower: object) -> None:
+ self._limiter.release_on_behalf_of(borrower)
+
+ def statistics(self) -> CapacityLimiterStatistics:
+ if self._internal_limiter is None:
+ return CapacityLimiterStatistics(
+ borrowed_tokens=0,
+ total_tokens=self.total_tokens,
+ borrowers=(),
+ tasks_waiting=0,
+ )
+
+ return self._internal_limiter.statistics()
+
+
+class ResourceGuard:
+ """
+ A context manager for ensuring that a resource is only used by a single task at a
+ time.
+
+ Entering this context manager while the previous has not exited it yet will trigger
+ :exc:`BusyResourceError`.
+
+ :param action: the action to guard against (visible in the :exc:`BusyResourceError`
+ when triggered, e.g. "Another task is already {action} this resource")
+
+ .. versionadded:: 4.1
+ """
+
+ __slots__ = "__weakref__", "_guarded", "action"
+
+ def __init__(self, action: str = "using"):
+ self.action: str = action
+ self._guarded = False
+
+ def __enter__(self) -> None:
+ if self._guarded:
+ raise BusyResourceError(self.action)
+
+ self._guarded = True
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self._guarded = False
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_tasks.py b/venv/lib/python3.11/site-packages/anyio/_core/_tasks.py
new file mode 100644
index 000000000..044eda6dc
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_tasks.py
@@ -0,0 +1,464 @@
+from __future__ import annotations
+
+import math
+import sys
+from collections.abc import (
+ Coroutine,
+ Generator,
+)
+from contextlib import contextmanager
+from enum import Enum, auto
+from inspect import iscoroutine
+from types import TracebackType
+from typing import Any, Generic, final
+
+from ..abc import TaskGroup, TaskStatus
+from ._eventloop import get_async_backend, get_cancelled_exc_class
+from ._exceptions import TaskCancelled, TaskFailed, TaskNotFinished
+
+if sys.version_info >= (3, 13):
+ from typing import TypeVar
+else:
+ from typing_extensions import TypeVar
+
+if sys.version_info >= (3, 11):
+ from typing import Never, Self, TypeVarTuple
+else:
+ from typing_extensions import Never, Self, TypeVarTuple
+
+T = TypeVar("T")
+T_co = TypeVar("T_co", covariant=True)
+T_startval_co = TypeVar("T_startval_co", covariant=True, default=Never)
+PosArgsT = TypeVarTuple("PosArgsT")
+
+
+class _IgnoredTaskStatus(TaskStatus[object]):
+ def started(self, value: object = None) -> None:
+ pass
+
+
+TASK_STATUS_IGNORED = _IgnoredTaskStatus()
+
+
+class CancelScope:
+ """
+ Wraps a unit of work that can be made separately cancellable.
+
+ :param deadline: The time (clock value) when this scope is cancelled automatically
+ :param shield: ``True`` to shield the cancel scope from external cancellation
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+ """
+
+ __slots__ = ("__weakref__",)
+
+ def __new__(
+ cls, *, deadline: float = math.inf, shield: bool = False
+ ) -> CancelScope:
+ return get_async_backend().create_cancel_scope(shield=shield, deadline=deadline)
+
+ def cancel(self, reason: str | None = None) -> None:
+ """
+ Cancel this scope immediately.
+
+ :param reason: a message describing the reason for the cancellation
+
+ """
+ raise NotImplementedError
+
+ @property
+ def deadline(self) -> float:
+ """
+ The time (clock value) when this scope is cancelled automatically.
+
+ Will be ``float('inf')`` if no timeout has been set.
+
+ """
+ raise NotImplementedError
+
+ @deadline.setter
+ def deadline(self, value: float) -> None:
+ raise NotImplementedError
+
+ @property
+ def cancel_called(self) -> bool:
+ """``True`` if :meth:`cancel` has been called."""
+ raise NotImplementedError
+
+ @property
+ def cancelled_caught(self) -> bool:
+ """
+ ``True`` if this scope suppressed a cancellation exception it itself raised.
+
+ This is typically used to check if any work was interrupted, or to see if the
+ scope was cancelled due to its deadline being reached. The value will, however,
+ only be ``True`` if the cancellation was triggered by the scope itself (and not
+ an outer scope).
+
+ """
+ raise NotImplementedError
+
+ @property
+ def shield(self) -> bool:
+ """
+ ``True`` if this scope is shielded from external cancellation.
+
+ While a scope is shielded, it will not receive cancellations from outside.
+
+ """
+ raise NotImplementedError
+
+ @shield.setter
+ def shield(self, value: bool) -> None:
+ raise NotImplementedError
+
+ def __enter__(self) -> Self:
+ raise NotImplementedError
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ raise NotImplementedError
+
+
+@contextmanager
+def fail_at(
+ deadline: float | None, shield: bool = False, reason: str | None = None
+) -> Generator[CancelScope, None, None]:
+ """
+ Create a context manager which raises a :class:`TimeoutError` if the code in the
+ enclosing context does not finish by the given deadline.
+
+ :param deadline: the deadline before raising the exception, or
+ ``None`` to disable the timeout
+ :param shield: ``True`` to shield the cancel scope from external cancellation
+ :param reason: explanation for timeout to add to the message of a raised `TimeoutError`
+ :return: a context manager that yields a cancel scope
+ :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\]
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ .. versionadded:: 4.15.0
+
+ """
+ current_time = get_async_backend().current_time
+ effective_deadline = math.inf if deadline is None else deadline
+ with get_async_backend().create_cancel_scope(
+ deadline=effective_deadline, shield=shield
+ ) as cancel_scope:
+ yield cancel_scope
+
+ if cancel_scope.cancelled_caught and current_time() >= cancel_scope.deadline:
+ raise TimeoutError(reason) if reason else TimeoutError
+
+
+@contextmanager
+def fail_after(
+ delay: float | None, shield: bool = False, reason: str | None = None
+) -> Generator[CancelScope, None, None]:
+ """
+ Create a context manager which raises a :class:`TimeoutError` if the code in the
+ enclosing context block does not finish in time.
+
+ :param delay: maximum allowed time (in seconds) before raising the exception, or
+ ``None`` to disable the timeout
+ :param shield: ``True`` to shield the cancel scope from external cancellation
+ :param reason: explanation for timeout to add to the message of a raised `TimeoutError`
+ :return: a context manager that yields a cancel scope
+ :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\]
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ current_time = get_async_backend().current_time
+ deadline = (current_time() + delay) if delay is not None else math.inf
+ with fail_at(deadline, shield=shield, reason=reason) as scope:
+ yield scope
+
+
+def move_on_at(deadline: float | None, shield: bool = False) -> CancelScope:
+ """
+ Create a cancel scope with a deadline that expires after the given delay.
+
+ :param deadline: the deadline before exiting the context block, or
+ ``None`` to disable the timeout
+ :param shield: ``True`` to shield the cancel scope from external cancellation
+ :return: a cancel scope
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ .. versionadded:: 4.15.0
+
+ """
+ return get_async_backend().create_cancel_scope(
+ deadline=deadline if deadline is not None else math.inf, shield=shield
+ )
+
+
+def move_on_after(delay: float | None, shield: bool = False) -> CancelScope:
+ """
+ Create a cancel scope with a deadline that expires after the given delay.
+
+ :param delay: maximum allowed time (in seconds) before exiting the context block, or
+ ``None`` to disable the timeout
+ :param shield: ``True`` to shield the cancel scope from external cancellation
+ :return: a cancel scope
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ .. note:: Unlike with :func:`fail_after`, the timer starts when this function is
+ called, not when the context manager is entered. This will be changed in v5.0.
+
+ """
+ deadline = (
+ (get_async_backend().current_time() + delay) if delay is not None else math.inf
+ )
+ return get_async_backend().create_cancel_scope(deadline=deadline, shield=shield)
+
+
+def current_effective_deadline() -> float:
+ """
+ Return the nearest deadline among all the cancel scopes effective for the current
+ task.
+
+ :return: a clock value from the event loop's internal clock (or ``float('inf')`` if
+ there is no deadline in effect, or ``float('-inf')`` if the current scope has
+ been cancelled)
+ :rtype: float
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().current_effective_deadline()
+
+
+def create_task_group() -> TaskGroup:
+ """
+ Create a task group.
+
+ :return: a task group
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().create_task_group()
+
+
+@final
+class TaskHandle(Generic[T_co, T_startval_co]):
+ """
+ Returned from the task-spawning methods of :class:`TaskGroup`. Can be awaited on to
+ get the return value of the task (or the raised exception). If the task was
+ terminated by a :exc:`BaseException`, :exc:`TaskFailed` will be raised (or its
+ subclass :exc:`TaskCancelled` if the task was cancelled).
+
+ .. versionadded:: 4.14.0
+ """
+
+ class Status(Enum):
+ """
+ The status of a task handle.
+
+ .. attribute:: PENDING
+
+ The task has not finished yet.
+ .. attribute:: FINISHED
+
+ The task has finished with a return value.
+ .. attribute:: CANCELLING
+
+ The task has been cancelled but has not finished yet.
+ .. attribute:: CANCELLED
+
+ The task was cancelled and has finished since.
+ .. attribute:: FAILED
+
+ The task raised an exception.
+ """
+
+ PENDING = auto()
+ FINISHED = auto()
+ CANCELLING = auto()
+ CANCELLED = auto()
+ FAILED = auto()
+
+ __slots__ = (
+ "__weakref__",
+ "_cancel_scope",
+ "_coro",
+ "_exception",
+ "_finished_event",
+ "_name",
+ "_return_value",
+ "_start_value",
+ )
+
+ _return_value: T_co
+ _start_value: T_startval_co
+
+ def __init__(
+ self,
+ coro: Coroutine[Any, Any, T_co],
+ name: object,
+ cancel_scope: CancelScope | None = None,
+ ) -> None:
+ from ._synchronization import Event
+
+ self._coro = coro
+ self._cancel_scope = cancel_scope if cancel_scope is not None else CancelScope()
+ self._finished_event = Event()
+ self._exception: BaseException | None = None
+
+ if name is not None:
+ self._name = str(name)
+ elif iscoroutine(coro):
+ self._name = coro.__qualname__
+ else:
+ self._name = str(coro) # coroutine-like object (e.g. asend() objects)
+
+ async def _run_coro(self) -> None:
+ __tracebackhide__ = True
+
+ with self._cancel_scope:
+ try:
+ retval = await self._coro
+ except BaseException as exc:
+ self._exception = exc
+ raise
+ else:
+ self._return_value = retval
+ finally:
+ self._finished_event.set()
+ del self # Break the reference cycle
+
+ def cancel(self) -> None:
+ """
+ Set the task to a cancelled state.
+
+ This will interrupt any interruptible asynchronous operation, and will cause
+ any further awaits on this task to get immediately cancelled, unless done in
+ a shielded cancel scope.
+
+ If the task has already finished, this method has no effect.
+ """
+ if not self._finished_event.is_set():
+ self._cancel_scope.cancel()
+
+ @property
+ def coro(self) -> Coroutine[Any, Any, T_co]:
+ """
+ The coroutine object that was passed to one of the task-spawning methods in
+ :class:`TaskGroup`.
+ """
+ return self._coro
+
+ @property
+ def status(self) -> TaskHandle.Status:
+ """
+ The current status of the task.
+
+ Every task starts in the :attr:`~TaskHandle.Status.PENDING` state.
+ If a task is cancelled while in this state, it will transition to the
+ :attr:`~TaskHandle.Status.CANCELLING` state. When the task finishes, it will
+ transition to one of the three final states (
+ :attr:`~TaskHandle.Status.FINISHED`, :attr:`~TaskHandle.Status.FAILED`, or
+ :attr:`~TaskHandle.Status.CANCELLING`) depending on the exception the task
+ raised, if any. No other status transitions will happen.
+ """
+ if not self._finished_event.is_set():
+ if self._cancel_scope.cancel_called:
+ return TaskHandle.Status.CANCELLING
+ else:
+ return TaskHandle.Status.PENDING
+ elif self._exception is not None:
+ if isinstance(self._exception, get_cancelled_exc_class()):
+ return TaskHandle.Status.CANCELLED
+ else:
+ return TaskHandle.Status.FAILED
+ else:
+ return TaskHandle.Status.FINISHED
+
+ @property
+ def name(self) -> str:
+ """The name of the task."""
+ return self._name
+
+ @property
+ def exception(self) -> BaseException | None:
+ """
+ The exception raised by the task, or ``None`` if it finished without raising.
+
+ :raises TaskNotFinished: if the task has not finished yet
+ :raises TaskCancelled: if the task was cancelled
+
+ """
+ match self.status:
+ case TaskHandle.Status.PENDING:
+ raise TaskNotFinished("the task has not finished yet")
+ case TaskHandle.Status.FINISHED:
+ return None
+ case TaskHandle.Status.CANCELLING:
+ raise TaskCancelled("the task was cancelled")
+ case TaskHandle.Status.CANCELLED:
+ raise TaskCancelled("the task was cancelled") from self._exception
+ case TaskHandle.Status.FAILED:
+ return self._exception
+
+ @property
+ def return_value(self) -> T_co:
+ """
+ The return value of the task.
+
+ :raises TaskNotFinished: if the task has not finished yet
+ :raises TaskCancelled: if the task was cancelled
+ :raises TaskFailed: if the task raised an exception
+
+ """
+ match self.status:
+ case TaskHandle.Status.PENDING:
+ raise TaskNotFinished("the task has not finished yet")
+ case TaskHandle.Status.FINISHED:
+ return self._return_value
+ case TaskHandle.Status.CANCELLING:
+ raise TaskCancelled("the task was cancelled")
+ case TaskHandle.Status.CANCELLED:
+ raise TaskCancelled("the task was cancelled") from self._exception
+ case TaskHandle.Status.FAILED:
+ raise TaskFailed("the task raised an exception") from self._exception
+
+ @property
+ def start_value(self) -> T_startval_co:
+ """
+ The value passed to :meth:`task_status.started() <.abc.TaskStatus.started>`,
+
+ :raises RuntimeError: if the task was not started with :meth:`TaskGroup.start()
+ <.abc.TaskGroup.start>`
+ """
+ try:
+ return self._start_value
+ except AttributeError:
+ raise RuntimeError(
+ "the task was not started with TaskGroup.start()"
+ ) from None
+
+ async def wait(self) -> None:
+ """
+ Wait for the task to finish.
+
+ This method will return as soon as the task has finished, no matter how it
+ happened.
+ """
+ await self._finished_event.wait()
+
+ def __await__(self) -> Generator[Any, Any, T_co]:
+ yield from self._finished_event.wait().__await__()
+ return self.return_value
+
+ def __repr__(self) -> str:
+ return (
+ f"<{self.__class__.__name__} {self.status.name.lower()} "
+ f"name={self._name!r} coro={self._coro!r}>"
+ )
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py b/venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py
new file mode 100644
index 000000000..96030867b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py
@@ -0,0 +1,615 @@
+from __future__ import annotations
+
+import os
+import sys
+import tempfile
+from collections.abc import Iterable
+from io import BytesIO, TextIOWrapper
+from types import TracebackType
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ AnyStr,
+ Generic,
+ overload,
+)
+
+from .. import to_thread
+from .._core._fileio import AsyncFile
+from ..lowlevel import checkpoint_if_cancelled
+from ._tasks import CancelScope
+
+if TYPE_CHECKING:
+ from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer
+
+
+class TemporaryFile(Generic[AnyStr]):
+ """
+ An asynchronous temporary file that is automatically created and cleaned up.
+
+ This class provides an asynchronous context manager interface to a temporary file.
+ The file is created using Python's standard `tempfile.TemporaryFile` function in a
+ background thread, and is wrapped as an asynchronous file using `AsyncFile`.
+
+ :param mode: The mode in which the file is opened. Defaults to "w+b".
+ :param buffering: The buffering policy (-1 means the default buffering).
+ :param encoding: The encoding used to decode or encode the file. Only applicable in
+ text mode.
+ :param newline: Controls how universal newlines mode works (only applicable in text
+ mode).
+ :param suffix: The suffix for the temporary file name.
+ :param prefix: The prefix for the temporary file name.
+ :param dir: The directory in which the temporary file is created.
+ :param errors: The error handling scheme used for encoding/decoding errors.
+ """
+
+ _async_file: AsyncFile[AnyStr]
+
+ @overload
+ def __init__(
+ self: TemporaryFile[bytes],
+ mode: OpenBinaryMode = ...,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ newline: str | None = ...,
+ suffix: str | None = ...,
+ prefix: str | None = ...,
+ dir: str | None = ...,
+ *,
+ errors: str | None = ...,
+ ): ...
+ @overload
+ def __init__(
+ self: TemporaryFile[str],
+ mode: OpenTextMode,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ newline: str | None = ...,
+ suffix: str | None = ...,
+ prefix: str | None = ...,
+ dir: str | None = ...,
+ *,
+ errors: str | None = ...,
+ ): ...
+
+ def __init__(
+ self,
+ mode: OpenTextMode | OpenBinaryMode = "w+b",
+ buffering: int = -1,
+ encoding: str | None = None,
+ newline: str | None = None,
+ suffix: str | None = None,
+ prefix: str | None = None,
+ dir: str | None = None,
+ *,
+ errors: str | None = None,
+ ) -> None:
+ self.mode = mode
+ self.buffering = buffering
+ self.encoding = encoding
+ self.newline = newline
+ self.suffix: str | None = suffix
+ self.prefix: str | None = prefix
+ self.dir: str | None = dir
+ self.errors = errors
+
+ async def __aenter__(self) -> AsyncFile[AnyStr]:
+ fp = await to_thread.run_sync(
+ lambda: tempfile.TemporaryFile(
+ self.mode,
+ self.buffering,
+ self.encoding,
+ self.newline,
+ self.suffix,
+ self.prefix,
+ self.dir,
+ errors=self.errors,
+ )
+ )
+ self._async_file = AsyncFile(fp)
+ return self._async_file
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ await self._async_file.aclose()
+
+
+class NamedTemporaryFile(Generic[AnyStr]):
+ """
+ An asynchronous named temporary file that is automatically created and cleaned up.
+
+ This class provides an asynchronous context manager for a temporary file with a
+ visible name in the file system. It uses Python's standard
+ :func:`~tempfile.NamedTemporaryFile` function and wraps the file object with
+ :class:`AsyncFile` for asynchronous operations.
+
+ :param mode: The mode in which the file is opened. Defaults to "w+b".
+ :param buffering: The buffering policy (-1 means the default buffering).
+ :param encoding: The encoding used to decode or encode the file. Only applicable in
+ text mode.
+ :param newline: Controls how universal newlines mode works (only applicable in text
+ mode).
+ :param suffix: The suffix for the temporary file name.
+ :param prefix: The prefix for the temporary file name.
+ :param dir: The directory in which the temporary file is created.
+ :param delete: Whether to delete the file when it is closed.
+ :param errors: The error handling scheme used for encoding/decoding errors.
+ :param delete_on_close: (Python 3.12+) Whether to delete the file on close.
+ """
+
+ _async_file: AsyncFile[AnyStr]
+
+ @overload
+ def __init__(
+ self: NamedTemporaryFile[bytes],
+ mode: OpenBinaryMode = ...,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ newline: str | None = ...,
+ suffix: str | None = ...,
+ prefix: str | None = ...,
+ dir: str | None = ...,
+ delete: bool = ...,
+ *,
+ errors: str | None = ...,
+ delete_on_close: bool = ...,
+ ): ...
+ @overload
+ def __init__(
+ self: NamedTemporaryFile[str],
+ mode: OpenTextMode,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ newline: str | None = ...,
+ suffix: str | None = ...,
+ prefix: str | None = ...,
+ dir: str | None = ...,
+ delete: bool = ...,
+ *,
+ errors: str | None = ...,
+ delete_on_close: bool = ...,
+ ): ...
+
+ def __init__(
+ self,
+ mode: OpenBinaryMode | OpenTextMode = "w+b",
+ buffering: int = -1,
+ encoding: str | None = None,
+ newline: str | None = None,
+ suffix: str | None = None,
+ prefix: str | None = None,
+ dir: str | None = None,
+ delete: bool = True,
+ *,
+ errors: str | None = None,
+ delete_on_close: bool = True,
+ ) -> None:
+ self._params: dict[str, Any] = {
+ "mode": mode,
+ "buffering": buffering,
+ "encoding": encoding,
+ "newline": newline,
+ "suffix": suffix,
+ "prefix": prefix,
+ "dir": dir,
+ "delete": delete,
+ "errors": errors,
+ }
+ if sys.version_info >= (3, 12):
+ self._params["delete_on_close"] = delete_on_close
+
+ async def __aenter__(self) -> AsyncFile[AnyStr]:
+ fp = await to_thread.run_sync(
+ lambda: tempfile.NamedTemporaryFile(**self._params)
+ )
+ self._async_file = AsyncFile(fp)
+ return self._async_file
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ await self._async_file.aclose()
+
+
+class SpooledTemporaryFile(AsyncFile[AnyStr]):
+ """
+ An asynchronous spooled temporary file that starts in memory and is spooled to disk.
+
+ This class provides an asynchronous interface to a spooled temporary file, much like
+ Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous
+ write operations and provides a method to force a rollover to disk.
+
+ :param max_size: Maximum size in bytes before the file is rolled over to disk.
+ :param mode: The mode in which the file is opened. Defaults to "w+b".
+ :param buffering: The buffering policy (-1 means the default buffering).
+ :param encoding: The encoding used to decode or encode the file (text mode only).
+ :param newline: Controls how universal newlines mode works (text mode only).
+ :param suffix: The suffix for the temporary file name.
+ :param prefix: The prefix for the temporary file name.
+ :param dir: The directory in which the temporary file is created.
+ :param errors: The error handling scheme used for encoding/decoding errors.
+ """
+
+ _rolled: bool = False
+
+ @overload
+ def __init__(
+ self: SpooledTemporaryFile[bytes],
+ max_size: int = ...,
+ mode: OpenBinaryMode = ...,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ newline: str | None = ...,
+ suffix: str | None = ...,
+ prefix: str | None = ...,
+ dir: str | None = ...,
+ *,
+ errors: str | None = ...,
+ ): ...
+ @overload
+ def __init__(
+ self: SpooledTemporaryFile[str],
+ max_size: int = ...,
+ mode: OpenTextMode = ...,
+ buffering: int = ...,
+ encoding: str | None = ...,
+ newline: str | None = ...,
+ suffix: str | None = ...,
+ prefix: str | None = ...,
+ dir: str | None = ...,
+ *,
+ errors: str | None = ...,
+ ): ...
+
+ def __init__(
+ self,
+ max_size: int = 0,
+ mode: OpenBinaryMode | OpenTextMode = "w+b",
+ buffering: int = -1,
+ encoding: str | None = None,
+ newline: str | None = None,
+ suffix: str | None = None,
+ prefix: str | None = None,
+ dir: str | None = None,
+ *,
+ errors: str | None = None,
+ ) -> None:
+ self._tempfile_params: dict[str, Any] = {
+ "mode": mode,
+ "buffering": buffering,
+ "encoding": encoding,
+ "newline": newline,
+ "suffix": suffix,
+ "prefix": prefix,
+ "dir": dir,
+ "errors": errors,
+ }
+ self._max_size = max_size
+ if "b" in mode:
+ super().__init__(BytesIO()) # type: ignore[arg-type]
+ else:
+ super().__init__(
+ TextIOWrapper( # type: ignore[arg-type]
+ BytesIO(),
+ encoding=encoding,
+ errors=errors,
+ newline=newline,
+ write_through=True,
+ )
+ )
+
+ async def aclose(self) -> None:
+ if not self._rolled:
+ self._fp.close()
+ return
+
+ await super().aclose()
+
+ async def _check(self) -> None:
+ if self._rolled or self._fp.tell() <= self._max_size:
+ return
+
+ await self.rollover()
+
+ async def rollover(self) -> None:
+ if self._rolled:
+ return
+
+ self._rolled = True
+ buffer = self._fp
+ buffer.seek(0)
+ self._fp = await to_thread.run_sync(
+ lambda: tempfile.TemporaryFile(**self._tempfile_params)
+ )
+ await self.write(buffer.read())
+ buffer.close()
+
+ @property
+ def closed(self) -> bool:
+ return self._fp.closed
+
+ async def read(self, size: int = -1) -> AnyStr:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.read(size)
+
+ return await super().read(size) # type: ignore[return-value]
+
+ async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.read1(size)
+
+ return await super().read1(size)
+
+ async def readline(self) -> AnyStr:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.readline()
+
+ return await super().readline() # type: ignore[return-value]
+
+ async def readlines(self) -> list[AnyStr]:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.readlines()
+
+ return await super().readlines() # type: ignore[return-value]
+
+ async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.readinto(b)
+
+ return await super().readinto(b)
+
+ async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.readinto1(b)
+
+ return await super().readinto1(b)
+
+ async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.seek(offset, whence)
+
+ return await super().seek(offset, whence)
+
+ async def tell(self) -> int:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.tell()
+
+ return await super().tell()
+
+ async def truncate(self, size: int | None = None) -> int:
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ return self._fp.truncate(size)
+
+ return await super().truncate(size)
+
+ @overload
+ async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ...
+ @overload
+ async def write(self: SpooledTemporaryFile[str], b: str) -> int: ...
+
+ async def write(self, b: ReadableBuffer | str) -> int:
+ """
+ Asynchronously write data to the spooled temporary file.
+
+ If the file has not yet been rolled over, the data is written synchronously,
+ and a rollover is triggered if the size exceeds the maximum size.
+
+ :param s: The data to write.
+ :return: The number of bytes written.
+ :raises RuntimeError: If the underlying file is not initialized.
+
+ """
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ result = self._fp.write(b)
+ await self._check()
+ return result
+
+ return await super().write(b) # type: ignore[misc]
+
+ @overload
+ async def writelines(
+ self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer]
+ ) -> None: ...
+ @overload
+ async def writelines(
+ self: SpooledTemporaryFile[str], lines: Iterable[str]
+ ) -> None: ...
+
+ async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None:
+ """
+ Asynchronously write a list of lines to the spooled temporary file.
+
+ If the file has not yet been rolled over, the lines are written synchronously,
+ and a rollover is triggered if the size exceeds the maximum size.
+
+ :param lines: An iterable of lines to write.
+ :raises RuntimeError: If the underlying file is not initialized.
+
+ """
+ if not self._rolled:
+ await checkpoint_if_cancelled()
+ result = self._fp.writelines(lines)
+ await self._check()
+ return result
+
+ return await super().writelines(lines) # type: ignore[misc]
+
+
+class TemporaryDirectory(Generic[AnyStr]):
+ """
+ An asynchronous temporary directory that is created and cleaned up automatically.
+
+ This class provides an asynchronous context manager for creating a temporary
+ directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to
+ perform directory creation and cleanup operations in a background thread.
+
+ :param suffix: Suffix to be added to the temporary directory name.
+ :param prefix: Prefix to be added to the temporary directory name.
+ :param dir: The parent directory where the temporary directory is created.
+ :param ignore_cleanup_errors: Whether to ignore errors during cleanup
+ :param delete: Whether to delete the directory upon closing (Python 3.12+).
+ """
+
+ def __init__(
+ self,
+ suffix: AnyStr | None = None,
+ prefix: AnyStr | None = None,
+ dir: AnyStr | None = None,
+ *,
+ ignore_cleanup_errors: bool = False,
+ delete: bool = True,
+ ) -> None:
+ self.suffix: AnyStr | None = suffix
+ self.prefix: AnyStr | None = prefix
+ self.dir: AnyStr | None = dir
+ self.ignore_cleanup_errors = ignore_cleanup_errors
+ self.delete = delete
+
+ self._tempdir: tempfile.TemporaryDirectory | None = None
+
+ async def __aenter__(self) -> str:
+ params: dict[str, Any] = {
+ "suffix": self.suffix,
+ "prefix": self.prefix,
+ "dir": self.dir,
+ "ignore_cleanup_errors": self.ignore_cleanup_errors,
+ }
+ if sys.version_info >= (3, 12):
+ params["delete"] = self.delete
+
+ self._tempdir = await to_thread.run_sync(
+ lambda: tempfile.TemporaryDirectory(**params)
+ )
+ return await to_thread.run_sync(self._tempdir.__enter__)
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ if self._tempdir is not None:
+ with CancelScope(shield=True):
+ await to_thread.run_sync(
+ self._tempdir.__exit__, exc_type, exc_value, traceback
+ )
+
+ async def cleanup(self) -> None:
+ if self._tempdir is not None:
+ await to_thread.run_sync(self._tempdir.cleanup)
+
+
+@overload
+async def mkstemp(
+ suffix: str | None = None,
+ prefix: str | None = None,
+ dir: str | None = None,
+ text: bool = False,
+) -> tuple[int, str]: ...
+
+
+@overload
+async def mkstemp(
+ suffix: bytes | None = None,
+ prefix: bytes | None = None,
+ dir: bytes | None = None,
+ text: bool = False,
+) -> tuple[int, bytes]: ...
+
+
+async def mkstemp(
+ suffix: AnyStr | None = None,
+ prefix: AnyStr | None = None,
+ dir: AnyStr | None = None,
+ text: bool = False,
+) -> tuple[int, str | bytes]:
+ """
+ Asynchronously create a temporary file and return an OS-level handle and the file
+ name.
+
+ This function wraps `tempfile.mkstemp` and executes it in a background thread.
+
+ :param suffix: Suffix to be added to the file name.
+ :param prefix: Prefix to be added to the file name.
+ :param dir: Directory in which the temporary file is created.
+ :param text: Whether the file is opened in text mode.
+ :return: A tuple containing the file descriptor and the file name.
+
+ """
+ return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text)
+
+
+@overload
+async def mkdtemp(
+ suffix: str | None = None,
+ prefix: str | None = None,
+ dir: str | None = None,
+) -> str: ...
+
+
+@overload
+async def mkdtemp(
+ suffix: bytes | None = None,
+ prefix: bytes | None = None,
+ dir: bytes | None = None,
+) -> bytes: ...
+
+
+async def mkdtemp(
+ suffix: AnyStr | None = None,
+ prefix: AnyStr | None = None,
+ dir: AnyStr | None = None,
+) -> str | bytes:
+ """
+ Asynchronously create a temporary directory and return its path.
+
+ This function wraps `tempfile.mkdtemp` and executes it in a background thread.
+
+ :param suffix: Suffix to be added to the directory name.
+ :param prefix: Prefix to be added to the directory name.
+ :param dir: Parent directory where the temporary directory is created.
+ :return: The path of the created temporary directory.
+
+ """
+ return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir)
+
+
+async def gettempdir() -> str:
+ """
+ Asynchronously return the name of the directory used for temporary files.
+
+ This function wraps `tempfile.gettempdir` and executes it in a background thread.
+
+ :return: The path of the temporary directory as a string.
+
+ """
+ return await to_thread.run_sync(tempfile.gettempdir)
+
+
+async def gettempdirb() -> bytes:
+ """
+ Asynchronously return the name of the directory used for temporary files in bytes.
+
+ This function wraps `tempfile.gettempdirb` and executes it in a background thread.
+
+ :return: The path of the temporary directory as bytes.
+
+ """
+ return await to_thread.run_sync(tempfile.gettempdirb)
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_testing.py b/venv/lib/python3.11/site-packages/anyio/_core/_testing.py
new file mode 100644
index 000000000..fa2c39137
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_testing.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from collections.abc import Awaitable, Generator
+from typing import Any, cast
+
+from ._eventloop import get_async_backend
+
+
+class TaskInfo:
+ """
+ Represents an asynchronous task.
+
+ :ivar int id: the unique identifier of the task
+ :ivar parent_id: the identifier of the parent task, if any
+ :vartype parent_id: Optional[int]
+ :ivar str name: the description of the task (if any)
+ :ivar ~collections.abc.Coroutine coro: the coroutine object of the task
+ """
+
+ __slots__ = "_name", "coro", "id", "name", "parent_id"
+
+ def __init__(
+ self,
+ id: int,
+ parent_id: int | None,
+ name: str | None,
+ coro: Generator[Any, Any, Any] | Awaitable[Any],
+ ):
+ func = get_current_task
+ self._name = f"{func.__module__}.{func.__qualname__}"
+ self.id: int = id
+ self.parent_id: int | None = parent_id
+ self.name: str | None = name
+ self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, TaskInfo):
+ return self.id == other.id
+
+ return NotImplemented
+
+ def __hash__(self) -> int:
+ return hash(self.id)
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})"
+
+ def has_pending_cancellation(self) -> bool:
+ """
+ Return ``True`` if the task has a cancellation pending, ``False`` otherwise.
+
+ """
+ return False
+
+
+def get_current_task() -> TaskInfo:
+ """
+ Return the current task.
+
+ :return: a representation of the current task
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().get_current_task()
+
+
+def get_running_tasks() -> list[TaskInfo]:
+ """
+ Return a list of running tasks in the current event loop.
+
+ :return: a list of task info objects
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return cast("list[TaskInfo]", get_async_backend().get_running_tasks())
+
+
+async def wait_all_tasks_blocked() -> None:
+ """Wait until all other tasks are waiting for something."""
+ await get_async_backend().wait_all_tasks_blocked()
diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py b/venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py
new file mode 100644
index 000000000..59d3da9d8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import sys
+from collections.abc import Callable, Mapping
+from typing import Any, TypeVar, final, overload
+
+from ._exceptions import TypedAttributeLookupError
+
+if sys.version_info < (3, 15):
+ from typing_extensions import sentinel
+
+T_Attr = TypeVar("T_Attr")
+T_Default = TypeVar("T_Default")
+undefined = sentinel("undefined")
+
+
+def typed_attribute() -> Any:
+ """Return a unique object, used to mark typed attributes."""
+ return object()
+
+
+class TypedAttributeSet:
+ """
+ Superclass for typed attribute collections.
+
+ Checks that every public attribute of every subclass has a type annotation.
+ """
+
+ def __init_subclass__(cls) -> None:
+ annotations: dict[str, Any] = getattr(cls, "__annotations__", {})
+ for attrname in dir(cls):
+ if not attrname.startswith("_") and attrname not in annotations:
+ raise TypeError(
+ f"Attribute {attrname!r} is missing its type annotation"
+ )
+
+ super().__init_subclass__()
+
+
+class TypedAttributeProvider:
+ """Base class for classes that wish to provide typed extra attributes."""
+
+ @property
+ def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]:
+ """
+ A mapping of the extra attributes to callables that return the corresponding
+ values.
+
+ If the provider wraps another provider, the attributes from that wrapper should
+ also be included in the returned mapping (but the wrapper may override the
+ callables from the wrapped instance).
+
+ """
+ return {}
+
+ @overload
+ def extra(self, attribute: T_Attr) -> T_Attr: ...
+
+ @overload
+ def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default: ...
+
+ @final
+ def extra(self, attribute: Any, default: object = undefined) -> object:
+ """
+ extra(attribute, default=undefined)
+
+ Return the value of the given typed extra attribute.
+
+ :param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to
+ look for
+ :param default: the value that should be returned if no value is found for the
+ attribute
+ :raises ~anyio.TypedAttributeLookupError: if the search failed and no default
+ value was given
+
+ """
+ try:
+ getter = self.extra_attributes[attribute]
+ except KeyError:
+ if default is undefined:
+ raise TypedAttributeLookupError("Attribute not found") from None
+ else:
+ return default
+
+ return getter()
diff --git a/venv/lib/python3.11/site-packages/anyio/_lazyimport.py b/venv/lib/python3.11/site-packages/anyio/_lazyimport.py
new file mode 100644
index 000000000..dc0a4faba
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/_lazyimport.py
@@ -0,0 +1,176 @@
+from __future__ import annotations
+
+__all__ = (
+ "fix_package_names",
+ "install_lazy_importer",
+ "set_deprecated_aliases",
+)
+
+import ast
+import inspect
+import sys
+import warnings
+from importlib import import_module
+from types import ModuleType
+from typing import Any
+
+
+def install_lazy_importer() -> bool:
+ module_globals = sys._getframe(1).f_globals
+ module_name = module_globals["__name__"]
+ module_prefix = module_name + "."
+ module = sys.modules[module_name]
+ lazy_map, deprecated_aliases, submodule_names = _build_lazy_map(module)
+ names = sorted(lazy_map)
+
+ # Delete symbols that are not part of the API
+ del module_globals["TYPE_CHECKING"]
+ del module_globals["install_lazy_importer"]
+
+ if not lazy_map and not deprecated_aliases:
+ return False
+
+ def __getattr__(name: str) -> Any:
+ if new_name := deprecated_aliases.get(name):
+ emit_deprecation_warning(module_name, name, new_name)
+ target_mod, target_attr = new_name.rsplit(".", 1)
+ elif name in submodule_names:
+ target_mod, target_attr = "." + name, ""
+ else:
+ try:
+ target_mod, target_attr = lazy_map[name]
+ except KeyError:
+ raise AttributeError(
+ f"module {module_name!r} has no attribute {name!r}"
+ ) from None
+
+ imported = import_module(target_mod, module_name)
+ value = getattr(imported, target_attr) if target_attr else imported
+
+ # patch the module name to match
+ if (
+ getattr(value, "__module__", "").startswith(module_prefix)
+ and name not in deprecated_aliases
+ ):
+ value.__module__ = module_name
+
+ module_globals[name] = value
+ return value
+
+ def __dir__() -> list[str]:
+ return names
+
+ module_globals["__dir__"] = __dir__
+ module_globals["__getattr__"] = __getattr__
+ module_globals.pop("fix_package_names", None)
+ module_globals.pop("set_deprecated_aliases", None)
+ return True
+
+
+def fix_package_names() -> None:
+ module_globals = sys._getframe(1).f_globals
+ module_prefix = module_globals["__name__"] + "."
+ del module_globals[fix_package_names.__name__]
+ for value in module_globals.values():
+ if modname := getattr(value, "__module__", ""):
+ if modname.startswith(module_prefix):
+ parts = modname.split(".")
+ value.__module__ = ".".join(
+ part for part in parts if not part.startswith("_")
+ )
+
+
+def emit_deprecation_warning(module_name: str, name: str, target: str) -> None:
+ warnings.warn(
+ f"The {module_name}.{name} alias is deprecated, use {target} instead.",
+ DeprecationWarning,
+ stacklevel=3,
+ )
+
+
+def set_deprecated_aliases(aliases: dict[str, str]) -> None:
+ module_globals = sys._getframe(1).f_globals
+ module_name = module_globals["__name__"]
+ del module_globals[set_deprecated_aliases.__name__]
+
+ def __getattr__(name: str) -> Any:
+ try:
+ target = aliases[name]
+ except KeyError:
+ raise AttributeError(
+ f"module {module_name!r} has no attribute {name!r}"
+ ) from None
+
+ emit_deprecation_warning(module_name, name, target)
+ target_modname, attrname = target.rsplit(".", 1)
+ module = import_module(target_modname)
+ return getattr(module, attrname)
+
+ sys.modules[module_name].__dict__["__getattr__"] = __getattr__
+
+
+def _build_lazy_map(
+ module: ModuleType,
+) -> tuple[dict[str, tuple[str, str]], dict[str, str], list[str]]:
+ try:
+ source = inspect.getsource(module)
+ except OSError:
+ return {}, {}, []
+
+ tree = compile(source, module.__file__ or "", "exec", ast.PyCF_ONLY_AST)
+ assert isinstance(tree, ast.Module)
+ out: dict[str, tuple[str, str]] = {}
+ deprecated_aliases: dict[str, str] = {}
+ submodule_names: list[str] = []
+
+ for node in tree.body:
+ if not isinstance(node, ast.If) or not _is_type_checking_block(node.test):
+ continue
+
+ for stmt in node.body:
+ match stmt:
+ case ast.ImportFrom():
+ if stmt.module is None:
+ submodule_names.extend(alias.name for alias in stmt.names)
+ else:
+ base = "." * stmt.level + (stmt.module or "")
+ for alias in stmt.names:
+ if alias.name == "*":
+ raise RuntimeError("star imports not supported")
+
+ exported = alias.asname or alias.name
+ out[exported] = (base, alias.name)
+ case ast.Expr() if isinstance(stmt.value, ast.Call):
+ call = stmt.value
+ if (
+ isinstance(call.func, ast.Name)
+ and call.func.id == "set_deprecated_aliases"
+ ):
+ arg0 = call.args[0]
+ assert isinstance(arg0, ast.Dict)
+ for key, value in zip(arg0.keys, arg0.values, strict=True):
+ assert isinstance(key, ast.Constant)
+ assert isinstance(key.value, str)
+ assert isinstance(value, ast.Constant)
+ assert isinstance(value.value, str)
+ deprecated_aliases[key.value] = value.value
+
+ return out, deprecated_aliases, submodule_names
+
+
+def _is_type_checking_block(test: ast.AST) -> bool:
+ if not isinstance(test, ast.BoolOp):
+ return False
+
+ subtest = test.values[0]
+ match subtest:
+ case ast.Name():
+ return subtest.id == "TYPE_CHECKING"
+ case ast.Attribute():
+ return (
+ isinstance(subtest.value, ast.Name)
+ and subtest.value.id == "typing"
+ and subtest.attr == "TYPE_CHECKING"
+ )
+ case _:
+ return False
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__init__.py b/venv/lib/python3.11/site-packages/anyio/abc/__init__.py
new file mode 100644
index 000000000..ba738f87f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/__init__.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from .._lazyimport import (
+ fix_package_names,
+ install_lazy_importer,
+ set_deprecated_aliases,
+)
+
+if TYPE_CHECKING or not install_lazy_importer():
+ from ._eventloop import AsyncBackend as AsyncBackend
+ from ._resources import AsyncResource as AsyncResource
+ from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket
+ from ._sockets import ConnectedUNIXDatagramSocket as ConnectedUNIXDatagramSocket
+ from ._sockets import IPAddressType as IPAddressType
+ from ._sockets import IPSockAddrType as IPSockAddrType
+ from ._sockets import SocketAttribute as SocketAttribute
+ from ._sockets import SocketListener as SocketListener
+ from ._sockets import SocketStream as SocketStream
+ from ._sockets import UDPPacketType as UDPPacketType
+ from ._sockets import UDPSocket as UDPSocket
+ from ._sockets import UNIXDatagramPacketType as UNIXDatagramPacketType
+ from ._sockets import UNIXDatagramSocket as UNIXDatagramSocket
+ from ._sockets import UNIXSocketStream as UNIXSocketStream
+ from ._streams import AnyByteReceiveStream as AnyByteReceiveStream
+ from ._streams import AnyByteSendStream as AnyByteSendStream
+ from ._streams import AnyByteStream as AnyByteStream
+ from ._streams import AnyByteStreamConnectable as AnyByteStreamConnectable
+ from ._streams import (
+ AnyUnreliableByteReceiveStream as AnyUnreliableByteReceiveStream,
+ )
+ from ._streams import AnyUnreliableByteSendStream as AnyUnreliableByteSendStream
+ from ._streams import AnyUnreliableByteStream as AnyUnreliableByteStream
+ from ._streams import ByteReceiveStream as ByteReceiveStream
+ from ._streams import ByteSendStream as ByteSendStream
+ from ._streams import ByteStream as ByteStream
+ from ._streams import ByteStreamConnectable as ByteStreamConnectable
+ from ._streams import Listener as Listener
+ from ._streams import ObjectReceiveStream as ObjectReceiveStream
+ from ._streams import ObjectSendStream as ObjectSendStream
+ from ._streams import ObjectStream as ObjectStream
+ from ._streams import ObjectStreamConnectable as ObjectStreamConnectable
+ from ._streams import UnreliableObjectReceiveStream as UnreliableObjectReceiveStream
+ from ._streams import UnreliableObjectSendStream as UnreliableObjectSendStream
+ from ._streams import UnreliableObjectStream as UnreliableObjectStream
+ from ._subprocesses import Process as Process
+ from ._tasks import TaskGroup as TaskGroup
+ from ._tasks import TaskStatus as TaskStatus
+ from ._testing import TestRunner as TestRunner
+
+ fix_package_names()
+ set_deprecated_aliases(
+ {
+ "CapacityLimiter": "anyio.CapacityLimiter",
+ "Condition": "anyio.Condition",
+ "Event": "anyio.Event",
+ "Lock": "anyio.Lock",
+ "Semaphore": "anyio.Semaphore",
+ "CancelScope": "anyio.CancelScope",
+ "BlockingPortal": "anyio.from_thread.BlockingPortal",
+ }
+ )
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py b/venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py
new file mode 100644
index 000000000..a3cbed2fe
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py
@@ -0,0 +1,428 @@
+from __future__ import annotations
+
+import math
+import sys
+from abc import ABCMeta, abstractmethod
+from collections.abc import (
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Coroutine,
+ Iterable,
+ Mapping,
+ Sequence,
+)
+from contextlib import AbstractContextManager
+from os import PathLike
+from signal import Signals
+from socket import AddressFamily, SocketKind, socket
+from typing import (
+ IO,
+ TYPE_CHECKING,
+ Any,
+ TypeAlias,
+ TypeVar,
+ overload,
+)
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+if TYPE_CHECKING:
+ from _typeshed import FileDescriptorLike
+
+ from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore
+ from .._core._tasks import CancelScope
+ from .._core._testing import TaskInfo
+ from ._sockets import (
+ ConnectedUDPSocket,
+ ConnectedUNIXDatagramSocket,
+ IPSockAddrType,
+ SocketListener,
+ SocketStream,
+ UDPSocket,
+ UNIXDatagramSocket,
+ UNIXSocketStream,
+ )
+ from ._subprocesses import Process
+ from ._tasks import TaskGroup
+ from ._testing import TestRunner
+
+T_Retval = TypeVar("T_Retval")
+T_co = TypeVar("T_co", covariant=True)
+PosArgsT = TypeVarTuple("PosArgsT")
+StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]
+
+
+class AsyncBackend(metaclass=ABCMeta):
+ @classmethod
+ @abstractmethod
+ def run(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
+ args: tuple[Unpack[PosArgsT]],
+ kwargs: dict[str, Any],
+ options: dict[str, Any],
+ ) -> T_Retval:
+ """
+ Run the given coroutine function in an asynchronous event loop.
+
+ The current thread must not be already running an event loop.
+
+ :param func: a coroutine function
+ :param args: positional arguments to ``func``
+ :param kwargs: positional arguments to ``func``
+ :param options: keyword arguments to call the backend ``run()`` implementation
+ with
+ :return: the return value of the coroutine function
+ """
+
+ @classmethod
+ @abstractmethod
+ def current_token(cls) -> object:
+ """
+ Return an object that allows other threads to run code inside the event loop.
+
+ :return: a token object, specific to the event loop running in the current
+ thread
+ """
+
+ @classmethod
+ @abstractmethod
+ def current_time(cls) -> float:
+ """
+ Return the current value of the event loop's internal clock.
+
+ :return: the clock value (seconds)
+ """
+
+ @classmethod
+ @abstractmethod
+ def cancelled_exception_class(cls) -> type[BaseException]:
+ """Return the exception class that is raised in a task if it's cancelled."""
+
+ @classmethod
+ @abstractmethod
+ async def checkpoint(cls) -> None:
+ """
+ Check if the task has been cancelled, and allow rescheduling of other tasks.
+
+ This is effectively the same as running :meth:`checkpoint_if_cancelled` and then
+ :meth:`cancel_shielded_checkpoint`.
+ """
+
+ @classmethod
+ async def checkpoint_if_cancelled(cls) -> None:
+ """
+ Check if the current task group has been cancelled.
+
+ This will check if the task has been cancelled, but will not allow other tasks
+ to be scheduled if not.
+
+ """
+ if cls.current_effective_deadline() == -math.inf:
+ await cls.checkpoint()
+
+ @classmethod
+ async def cancel_shielded_checkpoint(cls) -> None:
+ """
+ Allow the rescheduling of other tasks.
+
+ This will give other tasks the opportunity to run, but without checking if the
+ current task group has been cancelled, unlike with :meth:`checkpoint`.
+
+ """
+ with cls.create_cancel_scope(shield=True):
+ await cls.sleep(0)
+
+ @classmethod
+ @abstractmethod
+ async def sleep(cls, delay: float) -> None:
+ """
+ Pause the current task for the specified duration.
+
+ :param delay: the duration, in seconds
+ """
+
+ @classmethod
+ @abstractmethod
+ def create_cancel_scope(
+ cls, *, deadline: float = math.inf, shield: bool = False
+ ) -> CancelScope:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def current_effective_deadline(cls) -> float:
+ """
+ Return the nearest deadline among all the cancel scopes effective for the
+ current task.
+
+ :return:
+ - a clock value from the event loop's internal clock
+ - ``inf`` if there is no deadline in effect
+ - ``-inf`` if the current scope has been cancelled
+ :rtype: float
+ """
+
+ @classmethod
+ @abstractmethod
+ def create_task_group(cls) -> TaskGroup:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def create_event(cls) -> Event:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def create_lock(cls, *, fast_acquire: bool) -> Lock:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def create_semaphore(
+ cls,
+ initial_value: int,
+ *,
+ max_value: int | None = None,
+ fast_acquire: bool = False,
+ ) -> Semaphore:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def run_sync_in_worker_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ abandon_on_cancel: bool = False,
+ limiter: CapacityLimiter | None = None,
+ ) -> T_Retval:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def check_cancelled(cls) -> None:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def run_async_from_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_co:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def run_sync_from_thread(
+ cls,
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ token: object,
+ ) -> T_Retval:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def open_process(
+ cls,
+ command: StrOrBytesPath | Sequence[StrOrBytesPath],
+ *,
+ stdin: int | IO[Any] | None,
+ stdout: int | IO[Any] | None,
+ stderr: int | IO[Any] | None,
+ cwd: StrOrBytesPath | None = None,
+ env: Mapping[str, str] | None = None,
+ startupinfo: Any = None,
+ creationflags: int = 0,
+ start_new_session: bool = False,
+ pass_fds: Sequence[int] = (),
+ user: str | int | None = None,
+ group: str | int | None = None,
+ extra_groups: Iterable[str | int] | None = None,
+ umask: int = -1,
+ **kwargs: Any,
+ ) -> Process:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def setup_process_pool_exit_at_shutdown(cls, workers: set[Process]) -> None:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def connect_tcp(
+ cls, host: str, port: int, local_address: IPSockAddrType | None = None
+ ) -> SocketStream:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def connect_unix(cls, path: str | bytes) -> UNIXSocketStream:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def create_tcp_listener(cls, sock: socket) -> SocketListener:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def create_unix_listener(cls, sock: socket) -> SocketListener:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def create_udp_socket(
+ cls,
+ family: AddressFamily,
+ local_address: IPSockAddrType | None,
+ remote_address: IPSockAddrType | None,
+ reuse_port: bool,
+ ) -> UDPSocket | ConnectedUDPSocket:
+ pass
+
+ @classmethod
+ @overload
+ async def create_unix_datagram_socket(
+ cls, raw_socket: socket, remote_path: None
+ ) -> UNIXDatagramSocket: ...
+
+ @classmethod
+ @overload
+ async def create_unix_datagram_socket(
+ cls, raw_socket: socket, remote_path: str | bytes
+ ) -> ConnectedUNIXDatagramSocket: ...
+
+ @classmethod
+ @abstractmethod
+ async def create_unix_datagram_socket(
+ cls, raw_socket: socket, remote_path: str | bytes | None
+ ) -> UNIXDatagramSocket | ConnectedUNIXDatagramSocket:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def getaddrinfo(
+ cls,
+ host: bytes | str | None,
+ port: str | int | None,
+ *,
+ family: int | AddressFamily = 0,
+ type: int | SocketKind = 0,
+ proto: int = 0,
+ flags: int = 0,
+ ) -> Sequence[
+ tuple[
+ AddressFamily,
+ SocketKind,
+ int,
+ str,
+ tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes],
+ ]
+ ]:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def getnameinfo(
+ cls, sockaddr: IPSockAddrType, flags: int = 0
+ ) -> tuple[str, str]:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wait_readable(cls, obj: FileDescriptorLike) -> None:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wait_writable(cls, obj: FileDescriptorLike) -> None:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def notify_closing(cls, obj: FileDescriptorLike) -> None:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wrap_listener_socket(cls, sock: socket) -> SocketListener:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wrap_stream_socket(cls, sock: socket) -> SocketStream:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wrap_unix_stream_socket(cls, sock: socket) -> UNIXSocketStream:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wrap_udp_socket(cls, sock: socket) -> UDPSocket:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wrap_connected_udp_socket(cls, sock: socket) -> ConnectedUDPSocket:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wrap_unix_datagram_socket(cls, sock: socket) -> UNIXDatagramSocket:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wrap_connected_unix_datagram_socket(
+ cls, sock: socket
+ ) -> ConnectedUNIXDatagramSocket:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def current_default_thread_limiter(cls) -> CapacityLimiter:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def open_signal_receiver(
+ cls, *signals: Signals
+ ) -> AbstractContextManager[AsyncIterator[Signals]]:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def get_current_task(cls) -> TaskInfo:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def get_running_tasks(cls) -> Sequence[TaskInfo]:
+ pass
+
+ @classmethod
+ @abstractmethod
+ async def wait_all_tasks_blocked(cls) -> None:
+ pass
+
+ @classmethod
+ @abstractmethod
+ def create_test_runner(cls, options: dict[str, Any]) -> TestRunner:
+ pass
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_resources.py b/venv/lib/python3.11/site-packages/anyio/abc/_resources.py
new file mode 100644
index 000000000..9373f71a3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/_resources.py
@@ -0,0 +1,39 @@
+from __future__ import annotations
+
+import sys
+from abc import ABCMeta, abstractmethod
+from types import TracebackType
+from typing import TypeVar
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
+
+T = TypeVar("T")
+
+
+class AsyncResource(metaclass=ABCMeta):
+ """
+ Abstract base class for all closeable asynchronous resources.
+
+ Works as an asynchronous context manager which returns the instance itself on enter,
+ and calls :meth:`aclose` on exit.
+ """
+
+ __slots__ = ()
+
+ async def __aenter__(self) -> Self:
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ await self.aclose()
+
+ @abstractmethod
+ async def aclose(self) -> None:
+ """Close the resource."""
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_sockets.py b/venv/lib/python3.11/site-packages/anyio/abc/_sockets.py
new file mode 100644
index 000000000..d6a1139df
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/_sockets.py
@@ -0,0 +1,401 @@
+from __future__ import annotations
+
+import errno
+import socket
+from abc import abstractmethod
+from collections.abc import Callable, Collection, Mapping
+from contextlib import AsyncExitStack
+from io import IOBase
+from ipaddress import IPv4Address, IPv6Address
+from socket import AddressFamily
+from typing import TYPE_CHECKING, Any, TypeAlias, TypeVar
+
+from .._core._eventloop import get_async_backend
+from .._core._typedattr import (
+ TypedAttributeProvider,
+ TypedAttributeSet,
+ typed_attribute,
+)
+from ._streams import ByteStream, Listener, UnreliableObjectStream
+
+if TYPE_CHECKING:
+ from ._tasks import TaskGroup
+
+IPAddressType: TypeAlias = str | IPv4Address | IPv6Address
+IPSockAddrType: TypeAlias = tuple[str, int]
+SockAddrType: TypeAlias = IPSockAddrType | str
+UDPPacketType: TypeAlias = tuple[bytes, IPSockAddrType]
+UNIXDatagramPacketType: TypeAlias = tuple[bytes, str]
+T_Retval = TypeVar("T_Retval")
+
+
+def _validate_socket(
+ sock_or_fd: socket.socket | int,
+ sock_type: socket.SocketKind,
+ addr_family: socket.AddressFamily = socket.AF_UNSPEC,
+ *,
+ require_connected: bool = False,
+ require_bound: bool = False,
+) -> socket.socket:
+ if isinstance(sock_or_fd, int):
+ try:
+ sock = socket.socket(fileno=sock_or_fd)
+ except OSError as exc:
+ if exc.errno == errno.ENOTSOCK:
+ raise ValueError(
+ "the file descriptor does not refer to a socket"
+ ) from exc
+ elif require_connected:
+ raise ValueError("the socket must be connected") from exc
+ elif require_bound:
+ raise ValueError("the socket must be bound to a local address") from exc
+ else:
+ raise
+ elif isinstance(sock_or_fd, socket.socket):
+ sock = sock_or_fd
+ else:
+ raise TypeError(
+ f"expected an int or socket, got {type(sock_or_fd).__qualname__} instead"
+ )
+
+ try:
+ if require_connected:
+ try:
+ sock.getpeername()
+ except OSError as exc:
+ raise ValueError("the socket must be connected") from exc
+
+ if require_bound:
+ try:
+ if sock.family in (socket.AF_INET, socket.AF_INET6):
+ bound_addr = sock.getsockname()[1]
+ else:
+ bound_addr = sock.getsockname()
+ except OSError:
+ bound_addr = None
+
+ if not bound_addr:
+ raise ValueError("the socket must be bound to a local address")
+
+ if addr_family != socket.AF_UNSPEC and sock.family != addr_family:
+ raise ValueError(
+ f"address family mismatch: expected {addr_family.name}, got "
+ f"{sock.family.name}"
+ )
+
+ if sock.type != sock_type:
+ raise ValueError(
+ f"socket type mismatch: expected {sock_type.name}, got {sock.type.name}"
+ )
+ except BaseException:
+ # Avoid ResourceWarning from the locally constructed socket object
+ if isinstance(sock_or_fd, int):
+ sock.detach()
+
+ raise
+
+ sock.setblocking(False)
+ return sock
+
+
+class SocketAttribute(TypedAttributeSet):
+ """
+ .. attribute:: family
+ :type: socket.AddressFamily
+
+ the address family of the underlying socket
+
+ .. attribute:: local_address
+ :type: tuple[str, int] | str
+
+ the local address the underlying socket is connected to
+
+ .. attribute:: local_port
+ :type: int
+
+ for IP based sockets, the local port the underlying socket is bound to
+
+ .. attribute:: raw_socket
+ :type: socket.socket
+
+ the underlying stdlib socket object
+
+ .. attribute:: remote_address
+ :type: tuple[str, int] | str
+
+ the remote address the underlying socket is connected to
+
+ .. attribute:: remote_port
+ :type: int
+
+ for IP based sockets, the remote port the underlying socket is connected to
+ """
+
+ family: AddressFamily = typed_attribute()
+ local_address: SockAddrType = typed_attribute()
+ local_port: int = typed_attribute()
+ raw_socket: socket.socket = typed_attribute()
+ remote_address: SockAddrType = typed_attribute()
+ remote_port: int = typed_attribute()
+
+
+class _SocketProvider(TypedAttributeProvider):
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ from .._core._sockets import convert_ipv6_sockaddr as convert
+
+ attributes: dict[Any, Callable[[], Any]] = {
+ SocketAttribute.family: lambda: self._raw_socket.family,
+ SocketAttribute.local_address: lambda: convert(
+ self._raw_socket.getsockname()
+ ),
+ SocketAttribute.raw_socket: lambda: self._raw_socket,
+ }
+ try:
+ peername: tuple[str, int] | None = convert(self._raw_socket.getpeername())
+ except OSError:
+ peername = None
+
+ # Provide the remote address for connected sockets
+ if peername is not None:
+ attributes[SocketAttribute.remote_address] = lambda: peername
+
+ # Provide local and remote ports for IP based sockets
+ if self._raw_socket.family in (AddressFamily.AF_INET, AddressFamily.AF_INET6):
+ attributes[SocketAttribute.local_port] = lambda: (
+ self._raw_socket.getsockname()[1]
+ )
+ if peername is not None:
+ remote_port = peername[1]
+ attributes[SocketAttribute.remote_port] = lambda: remote_port
+
+ return attributes
+
+ @property
+ @abstractmethod
+ def _raw_socket(self) -> socket.socket:
+ pass
+
+
+class SocketStream(ByteStream, _SocketProvider):
+ """
+ Transports bytes over a socket.
+
+ Supports all relevant extra attributes from :class:`~SocketAttribute`.
+ """
+
+ @classmethod
+ async def from_socket(cls, sock_or_fd: socket.socket | int) -> SocketStream:
+ """
+ Wrap an existing socket object or file descriptor as a socket stream.
+
+ The newly created socket wrapper takes ownership of the socket being passed in.
+ The existing socket must already be connected.
+
+ :param sock_or_fd: a socket object or file descriptor
+ :return: a socket stream
+
+ """
+ sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_connected=True)
+ return await get_async_backend().wrap_stream_socket(sock)
+
+
+class UNIXSocketStream(SocketStream):
+ @classmethod
+ async def from_socket(cls, sock_or_fd: socket.socket | int) -> UNIXSocketStream:
+ """
+ Wrap an existing socket object or file descriptor as a UNIX socket stream.
+
+ The newly created socket wrapper takes ownership of the socket being passed in.
+ The existing socket must already be connected.
+
+ :param sock_or_fd: a socket object or file descriptor
+ :return: a UNIX socket stream
+
+ """
+ sock = _validate_socket(
+ sock_or_fd, socket.SOCK_STREAM, socket.AF_UNIX, require_connected=True
+ )
+ return await get_async_backend().wrap_unix_stream_socket(sock)
+
+ @abstractmethod
+ async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None:
+ """
+ Send file descriptors along with a message to the peer.
+
+ :param message: a non-empty bytestring
+ :param fds: a collection of files (either numeric file descriptors or open file
+ or socket objects)
+ """
+
+ @abstractmethod
+ async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]:
+ """
+ Receive file descriptors along with a message from the peer.
+
+ :param msglen: length of the message to expect from the peer
+ :param maxfds: maximum number of file descriptors to expect from the peer
+ :return: a tuple of (message, file descriptors)
+ """
+
+
+class SocketListener(Listener[SocketStream], _SocketProvider):
+ """
+ Listens to incoming socket connections.
+
+ Supports all relevant extra attributes from :class:`~SocketAttribute`.
+ """
+
+ @classmethod
+ async def from_socket(
+ cls,
+ sock_or_fd: socket.socket | int,
+ ) -> SocketListener:
+ """
+ Wrap an existing socket object or file descriptor as a socket listener.
+
+ The newly created listener takes ownership of the socket being passed in.
+
+ :param sock_or_fd: a socket object or file descriptor
+ :return: a socket listener
+
+ """
+ sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_bound=True)
+ return await get_async_backend().wrap_listener_socket(sock)
+
+ @abstractmethod
+ async def accept(self) -> SocketStream:
+ """Accept an incoming connection."""
+
+ async def serve(
+ self,
+ handler: Callable[[SocketStream], Any],
+ task_group: TaskGroup | None = None,
+ ) -> None:
+ from .. import create_task_group
+
+ async with AsyncExitStack() as stack:
+ if task_group is None:
+ task_group = await stack.enter_async_context(create_task_group())
+
+ while True:
+ stream = await self.accept()
+ task_group.start_soon(handler, stream)
+
+
+class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider):
+ """
+ Represents an unconnected UDP socket.
+
+ Supports all relevant extra attributes from :class:`~SocketAttribute`.
+ """
+
+ @classmethod
+ async def from_socket(cls, sock_or_fd: socket.socket | int) -> UDPSocket:
+ """
+ Wrap an existing socket object or file descriptor as a UDP socket.
+
+ The newly created socket wrapper takes ownership of the socket being passed in.
+ The existing socket must be bound to a local address.
+
+ :param sock_or_fd: a socket object or file descriptor
+ :return: a UDP socket
+
+ """
+ sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, require_bound=True)
+ return await get_async_backend().wrap_udp_socket(sock)
+
+ async def sendto(self, data: bytes, host: str, port: int) -> None:
+ """
+ Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, (host, port))).
+
+ """
+ return await self.send((data, (host, port)))
+
+
+class ConnectedUDPSocket(UnreliableObjectStream[bytes], _SocketProvider):
+ """
+ Represents an connected UDP socket.
+
+ Supports all relevant extra attributes from :class:`~SocketAttribute`.
+ """
+
+ @classmethod
+ async def from_socket(cls, sock_or_fd: socket.socket | int) -> ConnectedUDPSocket:
+ """
+ Wrap an existing socket object or file descriptor as a connected UDP socket.
+
+ The newly created socket wrapper takes ownership of the socket being passed in.
+ The existing socket must already be connected.
+
+ :param sock_or_fd: a socket object or file descriptor
+ :return: a connected UDP socket
+
+ """
+ sock = _validate_socket(
+ sock_or_fd,
+ socket.SOCK_DGRAM,
+ require_connected=True,
+ )
+ return await get_async_backend().wrap_connected_udp_socket(sock)
+
+
+class UNIXDatagramSocket(
+ UnreliableObjectStream[UNIXDatagramPacketType], _SocketProvider
+):
+ """
+ Represents an unconnected Unix datagram socket.
+
+ Supports all relevant extra attributes from :class:`~SocketAttribute`.
+ """
+
+ @classmethod
+ async def from_socket(
+ cls,
+ sock_or_fd: socket.socket | int,
+ ) -> UNIXDatagramSocket:
+ """
+ Wrap an existing socket object or file descriptor as a UNIX datagram
+ socket.
+
+ The newly created socket wrapper takes ownership of the socket being passed in.
+
+ :param sock_or_fd: a socket object or file descriptor
+ :return: a UNIX datagram socket
+
+ """
+ sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX)
+ return await get_async_backend().wrap_unix_datagram_socket(sock)
+
+ async def sendto(self, data: bytes, path: str) -> None:
+ """Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, path))."""
+ return await self.send((data, path))
+
+
+class ConnectedUNIXDatagramSocket(UnreliableObjectStream[bytes], _SocketProvider):
+ """
+ Represents a connected Unix datagram socket.
+
+ Supports all relevant extra attributes from :class:`~SocketAttribute`.
+ """
+
+ @classmethod
+ async def from_socket(
+ cls,
+ sock_or_fd: socket.socket | int,
+ ) -> ConnectedUNIXDatagramSocket:
+ """
+ Wrap an existing socket object or file descriptor as a connected UNIX datagram
+ socket.
+
+ The newly created socket wrapper takes ownership of the socket being passed in.
+ The existing socket must already be connected.
+
+ :param sock_or_fd: a socket object or file descriptor
+ :return: a connected UNIX datagram socket
+
+ """
+ sock = _validate_socket(
+ sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX, require_connected=True
+ )
+ return await get_async_backend().wrap_connected_unix_datagram_socket(sock)
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_streams.py b/venv/lib/python3.11/site-packages/anyio/abc/_streams.py
new file mode 100644
index 000000000..daf335dc3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/_streams.py
@@ -0,0 +1,237 @@
+from __future__ import annotations
+
+from abc import ABCMeta, abstractmethod
+from collections.abc import Callable
+from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
+
+from .._core._exceptions import EndOfStream
+from .._core._typedattr import TypedAttributeProvider
+from ._resources import AsyncResource
+
+if TYPE_CHECKING:
+ from ._tasks import TaskGroup
+
+T_Item = TypeVar("T_Item")
+T_co = TypeVar("T_co", covariant=True)
+T_contra = TypeVar("T_contra", contravariant=True)
+
+
+class UnreliableObjectReceiveStream(
+ AsyncResource, TypedAttributeProvider, Generic[T_co]
+):
+ """
+ An interface for receiving objects.
+
+ This interface makes no guarantees that the received messages arrive in the order in
+ which they were sent, or that no messages are missed.
+
+ Asynchronously iterating over objects of this type will yield objects matching the
+ given type parameter.
+ """
+
+ def __aiter__(self) -> UnreliableObjectReceiveStream[T_co]:
+ return self
+
+ async def __anext__(self) -> T_co:
+ try:
+ return await self.receive()
+ except EndOfStream:
+ raise StopAsyncIteration from None
+
+ @abstractmethod
+ async def receive(self) -> T_co:
+ """
+ Receive the next item.
+
+ :raises ~anyio.ClosedResourceError: if the receive stream has been explicitly
+ closed
+ :raises ~anyio.EndOfStream: if this stream has been closed from the other end
+ :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable
+ due to external causes
+ """
+
+
+class UnreliableObjectSendStream(
+ AsyncResource, TypedAttributeProvider, Generic[T_contra]
+):
+ """
+ An interface for sending objects.
+
+ This interface makes no guarantees that the messages sent will reach the
+ recipient(s) in the same order in which they were sent, or at all.
+ """
+
+ @abstractmethod
+ async def send(self, item: T_contra) -> None:
+ """
+ Send an item to the peer(s).
+
+ :param item: the item to send
+ :raises ~anyio.ClosedResourceError: if the send stream has been explicitly
+ closed
+ :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable
+ due to external causes
+ """
+
+
+class UnreliableObjectStream(
+ UnreliableObjectReceiveStream[T_Item], UnreliableObjectSendStream[T_Item]
+):
+ """
+ A bidirectional message stream which does not guarantee the order or reliability of
+ message delivery.
+ """
+
+
+class ObjectReceiveStream(UnreliableObjectReceiveStream[T_co]):
+ """
+ A receive message stream which guarantees that messages are received in the same
+ order in which they were sent, and that no messages are missed.
+ """
+
+
+class ObjectSendStream(UnreliableObjectSendStream[T_contra]):
+ """
+ A send message stream which guarantees that messages are delivered in the same order
+ in which they were sent, without missing any messages in the middle.
+ """
+
+
+class ObjectStream(
+ ObjectReceiveStream[T_Item],
+ ObjectSendStream[T_Item],
+ UnreliableObjectStream[T_Item],
+):
+ """
+ A bidirectional message stream which guarantees the order and reliability of message
+ delivery.
+ """
+
+ @abstractmethod
+ async def send_eof(self) -> None:
+ """
+ Send an end-of-file indication to the peer.
+
+ You should not try to send any further data to this stream after calling this
+ method. This method is idempotent (does nothing on successive calls).
+ """
+
+
+class ByteReceiveStream(AsyncResource, TypedAttributeProvider):
+ """
+ An interface for receiving bytes from a single peer.
+
+ Iterating this byte stream will yield a byte string of arbitrary length, but no more
+ than 65536 bytes.
+ """
+
+ def __aiter__(self) -> ByteReceiveStream:
+ return self
+
+ async def __anext__(self) -> bytes:
+ try:
+ return await self.receive()
+ except EndOfStream:
+ raise StopAsyncIteration from None
+
+ @abstractmethod
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ """
+ Receive at most ``max_bytes`` bytes from the peer.
+
+ .. note:: Implementers of this interface should not return an empty
+ :class:`bytes` object, and users should ignore them.
+
+ :param max_bytes: maximum number of bytes to receive (must be a positive
+ integer)
+ :return: the received bytes
+ :raises ValueError: if ``max_bytes`` is less than 1
+ :raises ~anyio.EndOfStream: if this stream has been closed from the other end
+ """
+
+
+class ByteSendStream(AsyncResource, TypedAttributeProvider):
+ """An interface for sending bytes to a single peer."""
+
+ @abstractmethod
+ async def send(self, item: bytes) -> None:
+ """
+ Send the given bytes to the peer.
+
+ :param item: the bytes to send
+ """
+
+
+class ByteStream(ByteReceiveStream, ByteSendStream):
+ """A bidirectional byte stream."""
+
+ @abstractmethod
+ async def send_eof(self) -> None:
+ """
+ Send an end-of-file indication to the peer.
+
+ You should not try to send any further data to this stream after calling this
+ method. This method is idempotent (does nothing on successive calls).
+ """
+
+
+#: Type alias for all unreliable bytes-oriented receive streams.
+AnyUnreliableByteReceiveStream: TypeAlias = (
+ UnreliableObjectReceiveStream[bytes] | ByteReceiveStream
+)
+#: Type alias for all unreliable bytes-oriented send streams.
+AnyUnreliableByteSendStream: TypeAlias = (
+ UnreliableObjectSendStream[bytes] | ByteSendStream
+)
+#: Type alias for all unreliable bytes-oriented streams.
+AnyUnreliableByteStream: TypeAlias = UnreliableObjectStream[bytes] | ByteStream
+#: Type alias for all bytes-oriented receive streams.
+AnyByteReceiveStream: TypeAlias = ObjectReceiveStream[bytes] | ByteReceiveStream
+#: Type alias for all bytes-oriented send streams.
+AnyByteSendStream: TypeAlias = ObjectSendStream[bytes] | ByteSendStream
+#: Type alias for all bytes-oriented streams.
+AnyByteStream: TypeAlias = ObjectStream[bytes] | ByteStream
+
+
+class Listener(AsyncResource, TypedAttributeProvider, Generic[T_co]):
+ """An interface for objects that let you accept incoming connections."""
+
+ @abstractmethod
+ async def serve(
+ self, handler: Callable[[T_co], Any], task_group: TaskGroup | None = None
+ ) -> None:
+ """
+ Accept incoming connections as they come in and start tasks to handle them.
+
+ :param handler: a callable that will be used to handle each accepted connection
+ :param task_group: the task group that will be used to start tasks for handling
+ each accepted connection (if omitted, an ad-hoc task group will be created)
+ """
+
+
+class ObjectStreamConnectable(Generic[T_co], metaclass=ABCMeta):
+ @abstractmethod
+ async def connect(self) -> ObjectStream[T_co]:
+ """
+ Connect to the remote endpoint.
+
+ :return: an object stream connected to the remote end
+ :raises ConnectionFailed: if the connection fails
+ """
+
+
+class ByteStreamConnectable(metaclass=ABCMeta):
+ @abstractmethod
+ async def connect(self) -> ByteStream:
+ """
+ Connect to the remote endpoint.
+
+ :return: a bytestream connected to the remote end
+ :raises ConnectionFailed: if the connection fails
+ """
+
+
+#: Type alias for all connectables returning bytestreams or bytes-oriented object streams
+AnyByteStreamConnectable: TypeAlias = (
+ ObjectStreamConnectable[bytes] | ByteStreamConnectable
+)
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_subprocesses.py b/venv/lib/python3.11/site-packages/anyio/abc/_subprocesses.py
new file mode 100644
index 000000000..555f15f10
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/_subprocesses.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from abc import abstractmethod
+from signal import Signals
+from typing import TYPE_CHECKING
+
+from ._resources import AsyncResource
+
+if TYPE_CHECKING:
+ from ._streams import ByteReceiveStream, ByteSendStream
+
+
+class Process(AsyncResource):
+ """An asynchronous version of :class:`subprocess.Popen`."""
+
+ @abstractmethod
+ async def wait(self) -> int:
+ """
+ Wait until the process exits.
+
+ :return: the exit code of the process
+ """
+
+ @abstractmethod
+ def terminate(self) -> None:
+ """
+ Terminates the process, gracefully if possible.
+
+ On Windows, this calls ``TerminateProcess()``.
+ On POSIX systems, this sends ``SIGTERM`` to the process.
+
+ .. seealso:: :meth:`subprocess.Popen.terminate`
+ """
+
+ @abstractmethod
+ def kill(self) -> None:
+ """
+ Kills the process.
+
+ On Windows, this calls ``TerminateProcess()``.
+ On POSIX systems, this sends ``SIGKILL`` to the process.
+
+ .. seealso:: :meth:`subprocess.Popen.kill`
+ """
+
+ @abstractmethod
+ def send_signal(self, signal: Signals) -> None:
+ """
+ Send a signal to the subprocess.
+
+ .. seealso:: :meth:`subprocess.Popen.send_signal`
+
+ :param signal: the signal number (e.g. :data:`signal.SIGHUP`)
+ """
+
+ @property
+ @abstractmethod
+ def pid(self) -> int:
+ """The process ID of the process."""
+
+ @property
+ @abstractmethod
+ def returncode(self) -> int | None:
+ """
+ The return code of the process. If the process has not yet terminated, this will
+ be ``None``.
+ """
+
+ @property
+ @abstractmethod
+ def stdin(self) -> ByteSendStream | None:
+ """The stream for the standard input of the process."""
+
+ @property
+ @abstractmethod
+ def stdout(self) -> ByteReceiveStream | None:
+ """The stream for the standard output of the process."""
+
+ @property
+ @abstractmethod
+ def stderr(self) -> ByteReceiveStream | None:
+ """The stream for the standard error output of the process."""
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_tasks.py b/venv/lib/python3.11/site-packages/anyio/abc/_tasks.py
new file mode 100644
index 000000000..0a019e2ed
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/_tasks.py
@@ -0,0 +1,223 @@
+from __future__ import annotations
+
+import sys
+from abc import ABCMeta, abstractmethod
+from collections.abc import Callable, Coroutine
+from contextvars import Context
+from types import TracebackType
+from typing import TYPE_CHECKING, Any, Literal, Protocol, final, overload
+
+if sys.version_info >= (3, 13):
+ from typing import TypeVar
+else:
+ from typing_extensions import TypeVar
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+if TYPE_CHECKING:
+ from .._core._tasks import CancelScope, TaskHandle
+
+T_co = TypeVar("T_co", covariant=True)
+T_contra = TypeVar("T_contra", contravariant=True, default=None)
+PosArgsT = TypeVarTuple("PosArgsT")
+
+
+def get_coro_name(coro: Coroutine[Any, Any, object], override: object = None) -> str:
+ if override is not None:
+ return str(override)
+
+ try:
+ cr_frame = coro.cr_frame # type: ignore[attr-defined]
+ module = cr_frame.f_globals["__name__"]
+ except (AttributeError, KeyError):
+ module = None
+
+ qualname = getattr(coro, "__qualname__", None)
+ return ".".join([x for x in (module, qualname) if x])
+
+
+def get_callable_name(func: Callable, override: object = None) -> str:
+ if override is not None:
+ return str(override)
+
+ module = getattr(func, "__module__", None)
+ qualname = getattr(func, "__qualname__", None)
+ return ".".join([x for x in (module, qualname) if x])
+
+
+def call_for_coroutine(
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ args: tuple[Unpack[PosArgsT]],
+ **kwargs: Any,
+) -> Coroutine[Any, Any, T_co]:
+ """
+ Call the given function with the given positional and keyword arguments.
+
+ :return: the resulting coroutine
+ :raises TypeError: if the return value was not a coroutine object
+
+ """
+ coro = func(*args, **kwargs)
+ if not isinstance(coro, Coroutine):
+ prefix = f"{func.__module__}." if hasattr(func, "__module__") else ""
+ raise TypeError(
+ f"Expected {prefix}{func.__qualname__}() to return a coroutine, but "
+ f"the return value ({coro!r}) is not a coroutine object"
+ )
+
+ return coro
+
+
+class TaskStatus(Protocol[T_contra]):
+ @overload
+ def started(self: TaskStatus[None]) -> None: ...
+
+ @overload
+ def started(self, value: T_contra) -> None: ...
+
+ def started(self, value: T_contra | None = None) -> None:
+ """
+ Signal that the task has started.
+
+ :param value: object passed back to the starter of the task
+ """
+
+
+class TaskGroup(metaclass=ABCMeta):
+ """
+ Groups several asynchronous tasks together.
+
+ :ivar cancel_scope: the cancel scope inherited by all child tasks
+ :vartype cancel_scope: CancelScope
+
+ .. note:: On asyncio, support for eager task factories is considered to be
+ **experimental**. In particular, they don't follow the usual semantics of new
+ tasks being scheduled on the next iteration of the event loop, and may thus
+ cause unexpected behavior in code that wasn't written with such semantics in
+ mind.
+ """
+
+ cancel_scope: CancelScope
+
+ def cancel(self, reason: str | None = None) -> None:
+ """
+ Cancel this task group's cancel scope immediately.
+
+ This is a shortcut for calling ``.cancel_scope.cancel()`` on the task group.
+
+ :param reason: a message describing the reason for the cancellation
+
+ .. versionadded:: 4.14.0
+
+ """
+ self.cancel_scope.cancel(reason)
+
+ @abstractmethod
+ def create_task(
+ self,
+ coro: Coroutine[Any, Any, T_co],
+ *,
+ name: object = None,
+ context: Context | None = None,
+ ) -> TaskHandle[T_co]:
+ """
+ Create a new task from a coroutine object and schedule it to run.
+
+ :param coro: a coroutine object
+ :param name: optional name to give the task
+ :param context: optional context to run the task in
+ :return: a task handle
+
+ .. versionadded:: 4.14.0
+ """
+
+ @final
+ def start_soon(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ ) -> TaskHandle[T_co]:
+ """
+ Start a new task in this task group.
+
+ :param func: a coroutine function
+ :param args: positional arguments to call the function with
+ :param name: name of the task, for the purposes of introspection and debugging
+ :return: a task handle
+
+ .. versionadded:: 3.0
+ .. versionchanged:: 4.14.0
+ This method now returns a task handle.
+
+ """
+ final_name = get_callable_name(func, name)
+ return self.create_task(call_for_coroutine(func, args), name=final_name)
+
+ @overload
+ async def start(
+ self,
+ func: Callable[..., Coroutine[Any, Any, T_co]],
+ *args: object,
+ name: object = None,
+ return_handle: Literal[False] = ...,
+ ) -> Any: ...
+
+ @overload
+ async def start(
+ self,
+ func: Callable[..., Coroutine[Any, Any, T_co]],
+ *args: object,
+ name: object = None,
+ return_handle: Literal[True],
+ ) -> TaskHandle[T_co, Any]: ...
+
+ @abstractmethod
+ async def start(
+ self,
+ func: Callable[..., Coroutine[Any, Any, T_co]],
+ *args: object,
+ name: object = None,
+ return_handle: Literal[False, True] = False,
+ ) -> Any:
+ """
+ Start a new task and wait until it signals for readiness.
+
+ The target callable must accept a keyword argument ``task_status`` (of type
+ :class:`TaskStatus`). Awaiting on this method will return whatever was passed to
+ ``task_status.started()`` (``None`` by default).
+
+ .. note:: The :class:`TaskStatus` class is generic, and the type argument should
+ indicate the type of the value that will be passed to
+ ``task_status.started()``.
+
+ :param func: a coroutine function that accepts the ``task_status`` keyword
+ argument
+ :param args: positional arguments to call the function with
+ :param name: an optional name for the task, for introspection and debugging
+ :param return_handle: if ``True``, return a :class:`TaskHandle` which also
+ contains the start value in ``start_value``
+ :return: the value passed to ``task_status.started()``
+ :raises RuntimeError: if the task finishes without calling
+ ``task_status.started()``
+
+ .. seealso:: :ref:`start_initialize`
+
+ .. versionadded:: 3.0
+ """
+
+ @abstractmethod
+ async def __aenter__(self) -> TaskGroup:
+ """Enter the task group context and allow starting new tasks."""
+
+ @abstractmethod
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ """Exit the task group context waiting for all tasks to finish."""
diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_testing.py b/venv/lib/python3.11/site-packages/anyio/abc/_testing.py
new file mode 100644
index 000000000..b18d8357e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/abc/_testing.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+import sys
+import types
+from abc import ABCMeta, abstractmethod
+from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable
+from typing import Any, TypeVar
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
+
+_T = TypeVar("_T")
+
+
+class TestRunner(metaclass=ABCMeta):
+ """
+ Encapsulates a running event loop. Every call made through this object will use the
+ same event loop.
+ """
+
+ def __enter__(self) -> Self:
+ return self
+
+ @abstractmethod
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: types.TracebackType | None,
+ ) -> bool | None: ...
+
+ @abstractmethod
+ def run_asyncgen_fixture(
+ self,
+ fixture_func: Callable[..., AsyncGenerator[_T, Any]],
+ kwargs: dict[str, Any],
+ ) -> Iterable[_T]:
+ """
+ Run an async generator fixture.
+
+ :param fixture_func: the fixture function
+ :param kwargs: keyword arguments to call the fixture function with
+ :return: an iterator yielding the value yielded from the async generator
+ """
+
+ @abstractmethod
+ def run_fixture(
+ self,
+ fixture_func: Callable[..., Coroutine[Any, Any, _T]],
+ kwargs: dict[str, Any],
+ ) -> _T:
+ """
+ Run an async fixture.
+
+ :param fixture_func: the fixture function
+ :param kwargs: keyword arguments to call the fixture function with
+ :return: the return value of the fixture function
+ """
+
+ @abstractmethod
+ def run_test(
+ self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any]
+ ) -> None:
+ """
+ Run an async test function.
+
+ :param test_func: the test function
+ :param kwargs: keyword arguments to call the test function with
+ """
+
+ @abstractmethod
+ def is_running(self) -> bool:
+ """
+ Check if the test runner is running.
+
+ :return: ``True`` if the coroutine is currently being run, ``False`` otherwise.
+ """
diff --git a/venv/lib/python3.11/site-packages/anyio/from_thread.py b/venv/lib/python3.11/site-packages/anyio/from_thread.py
new file mode 100644
index 000000000..f163d818b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/from_thread.py
@@ -0,0 +1,588 @@
+from __future__ import annotations
+
+__all__ = (
+ "BlockingPortal",
+ "BlockingPortalProvider",
+ "check_cancelled",
+ "run",
+ "run_sync",
+ "start_blocking_portal",
+)
+
+import sys
+from collections.abc import Awaitable, Callable, Coroutine, Generator
+from concurrent.futures import Future
+from contextlib import (
+ AbstractAsyncContextManager,
+ AbstractContextManager,
+ contextmanager,
+)
+from contextvars import Context
+from dataclasses import dataclass, field
+from functools import partial
+from inspect import isawaitable
+from threading import Lock, Thread, current_thread, get_ident
+from types import TracebackType
+from typing import (
+ Any,
+ Generic,
+ TypeVar,
+ cast,
+ overload,
+)
+
+from ._core._eventloop import (
+ get_cancelled_exc_class,
+ threadlocals,
+)
+from ._core._eventloop import run as run_eventloop
+from ._core._exceptions import NoEventLoopError
+from ._core._synchronization import Event
+from ._core._tasks import CancelScope, create_task_group
+from .abc._tasks import TaskStatus
+from .lowlevel import EventLoopToken, current_token
+
+if sys.version_info >= (3, 11):
+ from typing import Self, TypeVarTuple, Unpack
+else:
+ from typing_extensions import Self, TypeVarTuple, Unpack
+
+T_Retval = TypeVar("T_Retval")
+T_co = TypeVar("T_co", covariant=True)
+PosArgsT = TypeVarTuple("PosArgsT")
+
+
+def _token_or_error(token: EventLoopToken | None) -> EventLoopToken:
+ if token is not None:
+ return token
+
+ try:
+ return threadlocals.current_token
+ except AttributeError:
+ raise NoEventLoopError(
+ "Not running inside an AnyIO worker thread, and no event loop token was "
+ "provided"
+ ) from None
+
+
+def run(
+ func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]],
+ *args: Unpack[PosArgsT],
+ token: EventLoopToken | None = None,
+) -> T_co:
+ """
+ Call a coroutine function from a worker thread.
+
+ :param func: a coroutine function
+ :param args: positional arguments for the callable
+ :param token: an event loop token to use to get back to the event loop thread
+ (required if calling this function from outside an AnyIO worker thread)
+ :return: the return value of the coroutine function
+ :raises MissingTokenError: if no token was provided and called from outside an
+ AnyIO worker thread
+ :raises RunFinishedError: if the event loop tied to ``token`` is no longer running
+
+ .. versionchanged:: 4.11.0
+ Added the ``token`` parameter.
+
+ """
+ explicit_token = token is not None
+ token = _token_or_error(token)
+ return token.backend_class.run_async_from_thread(
+ func, args, token=token.native_token if explicit_token else None
+ )
+
+
+def run_sync(
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ *args: Unpack[PosArgsT],
+ token: EventLoopToken | None = None,
+) -> T_Retval:
+ """
+ Call a function in the event loop thread from a worker thread.
+
+ :param func: a callable
+ :param args: positional arguments for the callable
+ :param token: an event loop token to use to get back to the event loop thread
+ (required if calling this function from outside an AnyIO worker thread)
+ :return: the return value of the callable
+ :raises MissingTokenError: if no token was provided and called from outside an
+ AnyIO worker thread
+ :raises RunFinishedError: if the event loop tied to ``token`` is no longer running
+
+ .. versionchanged:: 4.11.0
+ Added the ``token`` parameter.
+
+ """
+ explicit_token = token is not None
+ token = _token_or_error(token)
+ return token.backend_class.run_sync_from_thread(
+ func, args, token=token.native_token if explicit_token else None
+ )
+
+
+class _BlockingAsyncContextManager(AbstractContextManager, Generic[T_co]):
+ _enter_future: Future[T_co]
+ _exit_future: Future[bool | None]
+ _exit_event: Event
+ _exit_exc_info: tuple[
+ type[BaseException] | None, BaseException | None, TracebackType | None
+ ] = (None, None, None)
+
+ def __init__(
+ self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal
+ ):
+ self._async_cm = async_cm
+ self._portal = portal
+
+ async def run_async_cm(self) -> bool | None:
+ try:
+ self._exit_event = Event()
+ value = await self._async_cm.__aenter__()
+ except BaseException as exc:
+ self._enter_future.set_exception(exc)
+ raise
+ else:
+ self._enter_future.set_result(value)
+
+ try:
+ # Wait for the sync context manager to exit.
+ # This next statement can raise `get_cancelled_exc_class()` if
+ # something went wrong in a task group in this async context
+ # manager.
+ await self._exit_event.wait()
+ finally:
+ # In case of cancellation, it could be that we end up here before
+ # `_BlockingAsyncContextManager.__exit__` is called, and an
+ # `_exit_exc_info` has been set.
+ result = await self._async_cm.__aexit__(*self._exit_exc_info)
+
+ return result
+
+ def __enter__(self) -> T_co:
+ self._enter_future = Future()
+ self._exit_future = self._portal.start_task_soon(self.run_async_cm)
+ return self._enter_future.result()
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ /,
+ ) -> bool | None:
+ self._exit_exc_info = exc_type, exc_value, traceback
+ self._portal.call(self._exit_event.set)
+ return self._exit_future.result()
+
+
+class _BlockingPortalTaskStatus(TaskStatus):
+ def __init__(self, future: Future):
+ self._future = future
+
+ def started(self, value: object = None) -> None:
+ self._future.set_result(value)
+
+
+class BlockingPortal:
+ """
+ An object that lets external threads run code in an asynchronous event loop.
+
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+ """
+
+ def __init__(self) -> None:
+ self._token = current_token()
+ self._event_loop_thread_id: int | None = get_ident()
+ self._stop_event = Event()
+ self._task_group = create_task_group()
+
+ async def __aenter__(self) -> Self:
+ await self._task_group.__aenter__()
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> bool:
+ await self.stop()
+ return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
+
+ def _check_running(self) -> None:
+ if self._event_loop_thread_id is None:
+ raise RuntimeError("This portal is not running")
+ if self._event_loop_thread_id == get_ident():
+ raise RuntimeError(
+ "This method cannot be called from the event loop thread"
+ )
+
+ async def sleep_until_stopped(self) -> None:
+ """Sleep until :meth:`stop` is called."""
+ await self._stop_event.wait()
+
+ async def stop(self, cancel_remaining: bool = False) -> None:
+ """
+ Signal the portal to shut down.
+
+ This marks the portal as no longer accepting new calls and exits from
+ :meth:`sleep_until_stopped`.
+
+ :param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False``
+ to let them finish before returning
+
+ """
+ self._event_loop_thread_id = None
+ self._stop_event.set()
+ if cancel_remaining:
+ self._task_group.cancel_scope.cancel("the blocking portal is shutting down")
+
+ async def _call_func(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ kwargs: dict[str, Any],
+ future: Future[T_Retval],
+ ) -> None:
+ event_loop_thread_id = self._event_loop_thread_id
+
+ def callback(f: Future[T_Retval]) -> None:
+ if f.cancelled():
+ if event_loop_thread_id == get_ident():
+ scope.cancel("the future was cancelled")
+ elif event_loop_thread_id is not None:
+ run_sync(
+ scope.cancel, "the future was cancelled", token=self._token
+ )
+
+ try:
+ retval_or_awaitable = func(*args, **kwargs)
+ if isawaitable(retval_or_awaitable):
+ with CancelScope() as scope:
+ future.add_done_callback(callback)
+ retval = await retval_or_awaitable
+ else:
+ retval = retval_or_awaitable
+ except get_cancelled_exc_class():
+ future.cancel()
+ future.set_running_or_notify_cancel()
+ except BaseException as exc:
+ if not future.cancelled():
+ future.set_exception(exc)
+
+ # Let base exceptions fall through
+ if not isinstance(exc, Exception):
+ raise
+ else:
+ if not future.cancelled():
+ future.set_result(retval)
+ finally:
+ scope = None # type: ignore[assignment]
+
+ def _spawn_task_from_thread(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
+ args: tuple[Unpack[PosArgsT]],
+ kwargs: dict[str, Any],
+ name: object,
+ future: Future[T_Retval],
+ ) -> None:
+ """
+ Spawn a new task using the given callable.
+
+ :param func: a callable
+ :param args: positional arguments to be passed to the callable
+ :param kwargs: keyword arguments to be passed to the callable
+ :param name: name of the task (will be coerced to a string if not ``None``)
+ :param future: a future that will resolve to the return value of the callable,
+ or the exception raised during its execution
+
+ """
+ run_sync(
+ partial(self._task_group.start_soon, name=name),
+ self._call_func,
+ func,
+ args,
+ kwargs,
+ future,
+ token=self._token,
+ )
+
+ @overload
+ def call(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
+ *args: Unpack[PosArgsT],
+ ) -> T_Retval: ...
+
+ @overload
+ def call(
+ self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT]
+ ) -> T_Retval: ...
+
+ def call(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
+ *args: Unpack[PosArgsT],
+ ) -> T_Retval:
+ """
+ Call the given function in the event loop thread.
+
+ If the callable returns a coroutine object, it is awaited on.
+
+ :param func: any callable
+ :raises RuntimeError: if the portal is not running or if this method is called
+ from within the event loop thread
+
+ """
+ return cast(T_Retval, self.start_task_soon(func, *args).result())
+
+ @overload
+ def start_task_soon(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ ) -> Future[T_Retval]: ...
+
+ @overload
+ def start_task_soon(
+ self,
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ ) -> Future[T_Retval]: ...
+
+ def start_task_soon(
+ self,
+ func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval],
+ *args: Unpack[PosArgsT],
+ name: object = None,
+ ) -> Future[T_Retval]:
+ """
+ Start a task in the portal's task group.
+
+ The task will be run inside a cancel scope which can be cancelled by cancelling
+ the returned future.
+
+ :param func: the target function
+ :param args: positional arguments passed to ``func``
+ :param name: name of the task (will be coerced to a string if not ``None``)
+ :return: a future that resolves with the return value of the callable if the
+ task completes successfully, or with the exception raised in the task
+ :raises RuntimeError: if the portal is not running or if this method is called
+ from within the event loop thread
+ :rtype: concurrent.futures.Future[T_Retval]
+
+ .. versionadded:: 3.0
+
+ """
+ self._check_running()
+ f: Future[T_Retval] = Future()
+ self._spawn_task_from_thread(func, args, {}, name, f)
+ return f
+
+ def start_task(
+ self,
+ func: Callable[..., Awaitable[T_Retval]],
+ *args: object,
+ name: object = None,
+ ) -> tuple[Future[T_Retval], Any]:
+ """
+ Start a task in the portal's task group and wait until it signals for readiness.
+
+ This method works the same way as :meth:`.abc.TaskGroup.start`.
+
+ :param func: the target function
+ :param args: positional arguments passed to ``func``
+ :param name: name of the task (will be coerced to a string if not ``None``)
+ :return: a tuple of (future, task_status_value) where the ``task_status_value``
+ is the value passed to ``task_status.started()`` from within the target
+ function
+ :rtype: tuple[concurrent.futures.Future[T_Retval], Any]
+
+ .. versionadded:: 3.0
+
+ """
+
+ def task_done(future: Future[T_Retval]) -> None:
+ if not task_status_future.done():
+ if future.cancelled():
+ task_status_future.cancel()
+ elif future.exception():
+ task_status_future.set_exception(future.exception())
+ else:
+ exc = RuntimeError(
+ "Task exited without calling task_status.started()"
+ )
+ task_status_future.set_exception(exc)
+
+ self._check_running()
+ task_status_future: Future = Future()
+ task_status = _BlockingPortalTaskStatus(task_status_future)
+ f: Future = Future()
+ f.add_done_callback(task_done)
+ self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f)
+ return f, task_status_future.result()
+
+ def wrap_async_context_manager(
+ self, cm: AbstractAsyncContextManager[T_co]
+ ) -> AbstractContextManager[T_co]:
+ """
+ Wrap an async context manager as a synchronous context manager via this portal.
+
+ Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping
+ in the middle until the synchronous context manager exits.
+
+ :param cm: an asynchronous context manager
+ :return: a synchronous context manager
+
+ .. versionadded:: 2.1
+
+ """
+ return _BlockingAsyncContextManager(cm, self)
+
+
+@dataclass
+class BlockingPortalProvider:
+ """
+ A manager for a blocking portal. Used as a context manager. The first thread to
+ enter this context manager causes a blocking portal to be started with the specific
+ parameters, and the last thread to exit causes the portal to be shut down. Thus,
+ there will be exactly one blocking portal running in this context as long as at
+ least one thread has entered this context manager.
+
+ The parameters are the same as for :func:`~anyio.run`.
+
+ :param backend: name of the backend
+ :param backend_options: backend options
+
+ .. versionadded:: 4.4
+ """
+
+ backend: str = "asyncio"
+ backend_options: dict[str, Any] | None = None
+ _lock: Lock = field(init=False, default_factory=Lock)
+ _leases: int = field(init=False, default=0)
+ _portal: BlockingPortal = field(init=False)
+ _portal_cm: AbstractContextManager[BlockingPortal] | None = field(
+ init=False, default=None
+ )
+
+ def __enter__(self) -> BlockingPortal:
+ with self._lock:
+ if self._portal_cm is None:
+ self._portal_cm = start_blocking_portal(
+ self.backend, self.backend_options
+ )
+ self._portal = self._portal_cm.__enter__()
+
+ self._leases += 1
+ return self._portal
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ portal_cm: AbstractContextManager[BlockingPortal] | None = None
+ with self._lock:
+ assert self._portal_cm
+ assert self._leases > 0
+ self._leases -= 1
+ if not self._leases:
+ portal_cm = self._portal_cm
+ self._portal_cm = None
+ del self._portal
+
+ if portal_cm:
+ portal_cm.__exit__(None, None, None)
+
+
+@contextmanager
+def start_blocking_portal(
+ backend: str = "asyncio",
+ backend_options: dict[str, Any] | None = None,
+ *,
+ name: str | None = None,
+) -> Generator[BlockingPortal, Any, None]:
+ """
+ Start a new event loop in a new thread and run a blocking portal in its main task.
+
+ The parameters are the same as for :func:`~anyio.run`.
+
+ :param backend: name of the backend
+ :param backend_options: backend options
+ :param name: name of the thread
+ :return: a context manager that yields a blocking portal
+
+ .. versionchanged:: 3.0
+ Usage as a context manager is now required.
+
+ """
+
+ async def run_portal() -> None:
+ async with BlockingPortal() as portal_:
+ if name is None:
+ current_thread().name = f"{backend}-portal-{id(portal_):x}"
+
+ future.set_result(portal_)
+ await portal_.sleep_until_stopped()
+
+ def run_blocking_portal() -> None:
+ if future.set_running_or_notify_cancel():
+ try:
+ run_eventloop(
+ run_portal, backend=backend, backend_options=backend_options
+ )
+ except BaseException as exc:
+ if not future.done():
+ future.set_exception(exc)
+
+ future: Future[BlockingPortal] = Future()
+ kwargs: dict[str, Any] = {}
+ if sys.version_info >= (3, 14):
+ kwargs["context"] = Context()
+
+ thread = Thread(target=run_blocking_portal, daemon=True, name=name, **kwargs)
+ thread.start()
+ try:
+ cancel_remaining_tasks = False
+ portal = future.result()
+ try:
+ yield portal
+ except BaseException:
+ cancel_remaining_tasks = True
+ raise
+ finally:
+ try:
+ portal.call(portal.stop, cancel_remaining_tasks)
+ except RuntimeError:
+ pass
+ finally:
+ thread.join()
+
+
+def check_cancelled() -> None:
+ """
+ Check if the cancel scope of the host task's running the current worker thread has
+ been cancelled.
+
+ If the host task's current cancel scope has indeed been cancelled, the
+ backend-specific cancellation exception will be raised.
+
+ :raises RuntimeError: if the current thread was not spawned by
+ :func:`.to_thread.run_sync`
+
+ """
+ try:
+ token: EventLoopToken = threadlocals.current_token
+ except AttributeError:
+ raise NoEventLoopError(
+ "This function can only be called inside an AnyIO worker thread"
+ ) from None
+
+ token.backend_class.check_cancelled()
diff --git a/venv/lib/python3.11/site-packages/anyio/functools.py b/venv/lib/python3.11/site-packages/anyio/functools.py
new file mode 100644
index 000000000..b0bdfb458
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/functools.py
@@ -0,0 +1,400 @@
+from __future__ import annotations
+
+__all__ = (
+ "AsyncCacheInfo",
+ "AsyncCacheParameters",
+ "AsyncLRUCacheWrapper",
+ "cache",
+ "lru_cache",
+ "reduce",
+)
+
+import functools
+from collections import OrderedDict
+from collections.abc import (
+ AsyncIterable,
+ Awaitable,
+ Callable,
+ Coroutine,
+ Hashable,
+ Iterable,
+)
+from functools import update_wrapper
+from inspect import iscoroutinefunction
+from typing import (
+ Any,
+ Generic,
+ NamedTuple,
+ ParamSpec,
+ TypedDict,
+ TypeVar,
+ cast,
+ final,
+ overload,
+)
+from weakref import WeakKeyDictionary
+
+from ._core._eventloop import current_time
+from ._core._synchronization import Lock
+from .lowlevel import RunVar, checkpoint
+
+T = TypeVar("T")
+S = TypeVar("S")
+P = ParamSpec("P")
+lru_cache_items: RunVar[
+ WeakKeyDictionary[
+ AsyncLRUCacheWrapper[Any, Any],
+ OrderedDict[
+ Hashable,
+ tuple[_InitialMissingType, Lock, float | None]
+ | tuple[Any, None, float | None],
+ ],
+ ]
+] = RunVar("lru_cache_items")
+
+
+class _InitialMissingType:
+ pass
+
+
+initial_missing: _InitialMissingType = _InitialMissingType()
+
+
+class AsyncCacheInfo(NamedTuple):
+ hits: int
+ misses: int
+ maxsize: int | None
+ currsize: int
+ ttl: int | None
+
+
+class AsyncCacheParameters(TypedDict):
+ maxsize: int | None
+ typed: bool
+ always_checkpoint: bool
+ ttl: int | None
+
+
+class _LRUMethodWrapper(Generic[T]):
+ def __init__(self, wrapper: AsyncLRUCacheWrapper[..., T], instance: object):
+ self.__wrapper = wrapper
+ self.__instance = instance
+
+ def cache_info(self) -> AsyncCacheInfo:
+ return self.__wrapper.cache_info()
+
+ def cache_parameters(self) -> AsyncCacheParameters:
+ return self.__wrapper.cache_parameters()
+
+ def cache_clear(self) -> None:
+ self.__wrapper.cache_clear()
+
+ async def __call__(self, *args: Any, **kwargs: Any) -> T:
+ if self.__instance is None:
+ return await self.__wrapper(*args, **kwargs)
+
+ return await self.__wrapper(self.__instance, *args, **kwargs)
+
+
+@final
+class AsyncLRUCacheWrapper(Generic[P, T]):
+ def __init__(
+ self,
+ func: Callable[P, Awaitable[T]],
+ maxsize: int | None,
+ typed: bool,
+ always_checkpoint: bool,
+ ttl: int | None,
+ ):
+ self.__wrapped__ = func
+ self._hits: int = 0
+ self._misses: int = 0
+ self._maxsize = max(maxsize, 0) if maxsize is not None else None
+ self._currsize: int = 0
+ self._typed = typed
+ self._always_checkpoint = always_checkpoint
+ self._ttl = ttl
+ update_wrapper(self, func)
+
+ def cache_info(self) -> AsyncCacheInfo:
+ return AsyncCacheInfo(
+ self._hits, self._misses, self._maxsize, self._currsize, self._ttl
+ )
+
+ def cache_parameters(self) -> AsyncCacheParameters:
+ return {
+ "maxsize": self._maxsize,
+ "typed": self._typed,
+ "always_checkpoint": self._always_checkpoint,
+ "ttl": self._ttl,
+ }
+
+ def cache_clear(self) -> None:
+ if cache := lru_cache_items.get(None):
+ cache.pop(self, None)
+ self._hits = self._misses = self._currsize = 0
+
+ async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
+ # Easy case first: if maxsize == 0, no caching is done
+ if self._maxsize == 0:
+ value = await self.__wrapped__(*args, **kwargs)
+ self._misses += 1
+ return value
+
+ # The key is constructed as a flat tuple to avoid memory overhead
+ key: tuple[Any, ...] = args
+ if kwargs:
+ # initial_missing is used as a separator
+ key += (initial_missing,) + sum(kwargs.items(), ())
+
+ if self._typed:
+ key += tuple(type(arg) for arg in args)
+ if kwargs:
+ key += (initial_missing,) + tuple(type(val) for val in kwargs.values())
+
+ try:
+ cache = lru_cache_items.get()
+ except LookupError:
+ cache = WeakKeyDictionary()
+ lru_cache_items.set(cache)
+
+ try:
+ cache_entry = cache[self]
+ except KeyError:
+ cache_entry = cache[self] = OrderedDict()
+
+ cached_value: T | _InitialMissingType
+ try:
+ cached_value, lock, expires_at = cache_entry[key]
+ except KeyError:
+ # We're the first task to call this function
+ cached_value, lock, expires_at = (
+ initial_missing,
+ Lock(fast_acquire=not self._always_checkpoint),
+ None,
+ )
+ cache_entry[key] = cached_value, lock, expires_at
+
+ if lock is None:
+ if expires_at is not None and current_time() >= expires_at:
+ self._currsize -= 1
+ cached_value, lock, expires_at = (
+ initial_missing,
+ Lock(fast_acquire=not self._always_checkpoint),
+ None,
+ )
+ cache_entry[key] = cached_value, lock, expires_at
+ else:
+ # The value was already cached
+ self._hits += 1
+ cache_entry.move_to_end(key)
+ if self._always_checkpoint:
+ await checkpoint()
+
+ return cast(T, cached_value)
+
+ async with lock:
+ # Check if another task filled the cache while we acquired the lock
+ if (cached_value := cache_entry[key][0]) is initial_missing:
+ self._misses += 1
+ if self._maxsize is not None and self._currsize >= self._maxsize:
+ cache_entry.popitem(last=False)
+ else:
+ self._currsize += 1
+
+ value = await self.__wrapped__(*args, **kwargs)
+ expires_at = (
+ current_time() + self._ttl if self._ttl is not None else None
+ )
+ cache_entry[key] = value, None, expires_at
+ else:
+ # Another task filled the cache while we were waiting for the lock
+ self._hits += 1
+ cache_entry.move_to_end(key)
+ value = cast(T, cached_value)
+
+ return value
+
+ def __get__(
+ self, instance: object, owner: type | None = None
+ ) -> _LRUMethodWrapper[T]:
+ wrapper = _LRUMethodWrapper(self, instance)
+ update_wrapper(wrapper, self.__wrapped__)
+ return wrapper
+
+
+class _LRUCacheWrapper:
+ def __init__(
+ self, maxsize: int | None, typed: bool, always_checkpoint: bool, ttl: int | None
+ ):
+ self._maxsize = maxsize
+ self._typed = typed
+ self._always_checkpoint = always_checkpoint
+ self._ttl = ttl
+
+ @overload
+ def __call__( # type: ignore[overload-overlap]
+ self, func: Callable[P, Coroutine[Any, Any, T]], /
+ ) -> AsyncLRUCacheWrapper[P, T]: ...
+
+ @overload
+ def __call__(
+ self, func: Callable[..., T], /
+ ) -> functools._lru_cache_wrapper[T]: ...
+
+ def __call__(
+ self, f: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T], /
+ ) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]:
+ if iscoroutinefunction(f):
+ return AsyncLRUCacheWrapper(
+ f, self._maxsize, self._typed, self._always_checkpoint, self._ttl
+ )
+
+ return functools.lru_cache(maxsize=self._maxsize, typed=self._typed)(f) # type: ignore[arg-type]
+
+
+@overload
+def cache( # type: ignore[overload-overlap]
+ func: Callable[P, Coroutine[Any, Any, T]], /
+) -> AsyncLRUCacheWrapper[P, T]: ...
+
+
+@overload
+def cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
+
+
+def cache(func: Callable[..., Any] | Callable[P, Coroutine[Any, Any, Any]], /) -> Any:
+ """
+ A convenient shortcut for :func:`lru_cache` with ``maxsize=None``.
+
+ This is the asynchronous equivalent to :func:`functools.cache`.
+
+ """
+ return lru_cache(maxsize=None)(func)
+
+
+@overload
+def lru_cache(
+ *,
+ maxsize: int | None = ...,
+ typed: bool = ...,
+ always_checkpoint: bool = ...,
+ ttl: int | None = ...,
+) -> _LRUCacheWrapper: ...
+
+
+@overload
+def lru_cache( # type: ignore[overload-overlap]
+ func: Callable[P, Coroutine[Any, Any, T]], /
+) -> AsyncLRUCacheWrapper[P, T]: ...
+
+
+@overload
+def lru_cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ...
+
+
+def lru_cache(
+ func: Callable[..., Coroutine[Any, Any, Any]] | Callable[..., Any] | None = None,
+ /,
+ *,
+ maxsize: int | None = 128,
+ typed: bool = False,
+ always_checkpoint: bool = False,
+ ttl: int | None = None,
+) -> Any:
+ """
+ An asynchronous version of :func:`functools.lru_cache`.
+
+ If a synchronous function is passed, the standard library
+ :func:`functools.lru_cache` is applied instead.
+
+ :param always_checkpoint: if ``True``, every call to the cached function will be
+ guaranteed to yield control to the event loop at least once
+ :param ttl: time in seconds after which to invalidate cache entries
+
+ .. note:: Caches and locks are managed on a per-event loop basis.
+
+ """
+ if func is None:
+ return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl)
+
+ if not callable(func):
+ raise TypeError("the first argument must be callable")
+
+ return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl)(func)
+
+
+@overload
+async def reduce(
+ function: Callable[[T, S], Awaitable[T]],
+ iterable: Iterable[S] | AsyncIterable[S],
+ /,
+ initial: T,
+) -> T: ...
+
+
+@overload
+async def reduce(
+ function: Callable[[T, T], Awaitable[T]],
+ iterable: Iterable[T] | AsyncIterable[T],
+ /,
+) -> T: ...
+
+
+async def reduce( # type: ignore[misc]
+ function: Callable[[T, T], Awaitable[T]] | Callable[[T, S], Awaitable[T]],
+ iterable: Iterable[T] | Iterable[S] | AsyncIterable[T] | AsyncIterable[S],
+ /,
+ initial: T | _InitialMissingType = initial_missing,
+) -> T:
+ """
+ Asynchronous version of :func:`functools.reduce`.
+
+ :param function: a coroutine function that takes two arguments: the accumulated
+ value and the next element from the iterable
+ :param iterable: an iterable or async iterable
+ :param initial: the initial value (if missing, the first element of the iterable is
+ used as the initial value)
+
+ """
+ element: Any
+ function_called = False
+ if isinstance(iterable, AsyncIterable):
+ async_it = iterable.__aiter__()
+ if initial is initial_missing:
+ try:
+ value = cast(T, await async_it.__anext__())
+ except StopAsyncIteration:
+ raise TypeError(
+ "reduce() of empty sequence with no initial value"
+ ) from None
+ else:
+ value = cast(T, initial)
+
+ async for element in async_it:
+ value = await function(value, element)
+ function_called = True
+ elif isinstance(iterable, Iterable):
+ it = iter(iterable)
+ if initial is initial_missing:
+ try:
+ value = cast(T, next(it))
+ except StopIteration:
+ raise TypeError(
+ "reduce() of empty sequence with no initial value"
+ ) from None
+ else:
+ value = cast(T, initial)
+
+ for element in it:
+ value = await function(value, element)
+ function_called = True
+ else:
+ raise TypeError("reduce() argument 2 must be an iterable or async iterable")
+
+ # Make sure there is at least one checkpoint, even if an empty iterable and an
+ # initial value were given
+ if not function_called:
+ await checkpoint()
+
+ return value
diff --git a/venv/lib/python3.11/site-packages/anyio/itertools.py b/venv/lib/python3.11/site-packages/anyio/itertools.py
new file mode 100644
index 000000000..2a10cfd3b
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/itertools.py
@@ -0,0 +1,629 @@
+from __future__ import annotations
+
+__all__ = (
+ "Chain",
+ "accumulate",
+ "batched",
+ "combinations",
+ "combinations_with_replacement",
+ "compress",
+ "count",
+ "cycle",
+ "dropwhile",
+ "filterfalse",
+ "groupby",
+ "islice",
+ "pairwise",
+ "permutations",
+ "product",
+ "repeat",
+ "starmap",
+ "takewhile",
+ "tee",
+ "zip_longest",
+)
+
+import itertools
+import operator
+import sys
+from collections.abc import (
+ AsyncGenerator,
+ AsyncIterable,
+ AsyncIterator,
+ Awaitable,
+ Callable,
+ Iterable,
+ Iterator,
+)
+from dataclasses import dataclass, field
+from typing import Any, Generic, TypeVar, cast, overload
+
+from ._core._synchronization import Lock
+from ._core._tasks import CancelScope
+from .lowlevel import cancel_shielded_checkpoint, checkpoint, checkpoint_if_cancelled
+
+if sys.version_info < (3, 15):
+ from typing_extensions import sentinel
+
+T = TypeVar("T")
+R = TypeVar("R")
+_tee_end = sentinel("_tee_end")
+
+
+@dataclass(eq=False)
+class _IterableAsyncIterator(AsyncIterator[T]):
+ iterator: Iterator[T]
+
+ async def __anext__(self) -> T:
+ await checkpoint_if_cancelled()
+ try:
+ result = next(self.iterator)
+ except StopIteration:
+ await cancel_shielded_checkpoint()
+ raise StopAsyncIteration from None
+
+ await cancel_shielded_checkpoint()
+ return result
+
+
+def _iterate(iterable: Iterable[T] | AsyncIterable[T]) -> AsyncIterator[T]:
+ if isinstance(iterable, AsyncIterator):
+ return iterable
+
+ if isinstance(iterable, AsyncIterable):
+ return iterable.__aiter__()
+
+ return _IterableAsyncIterator(iter(iterable))
+
+
+@dataclass(eq=False)
+class _TeeLink(Generic[T]):
+ value: object | None = None
+ next: _TeeLink[T] | None = None
+ filled: bool = False
+
+
+@dataclass(eq=False)
+class _TeeState(Generic[T]):
+ iterator: AsyncIterator[T]
+ lock: Lock = field(default_factory=Lock)
+
+ async def fill(self, link: _TeeLink[T]) -> bool:
+ if link.filled:
+ return False
+
+ async with self.lock:
+ if link.filled:
+ return True
+
+ link.value = await anext(self.iterator, _tee_end)
+ if link.value is not _tee_end:
+ link.next = _TeeLink()
+
+ link.filled = True
+ return True
+
+
+class _TeeAsyncIterator(AsyncIterator[T]):
+ _state: _TeeState[T]
+ _link: _TeeLink[T]
+ _element_yielded: bool
+
+ def __init__(
+ self, iterable: Iterable[T] | AsyncIterable[T] | _TeeAsyncIterator[T]
+ ) -> None:
+ if isinstance(iterable, _TeeAsyncIterator):
+ self._state = iterable._state
+ self._link = iterable._link
+ else:
+ self._state = _TeeState(_iterate(iterable))
+ self._link = _TeeLink()
+
+ self._element_yielded = False
+
+ async def __anext__(self) -> T:
+ had_yieldpoint = await self._state.fill(self._link)
+ if self._link.value is _tee_end:
+ if not self._element_yielded:
+ await checkpoint()
+
+ raise StopAsyncIteration
+
+ if not had_yieldpoint:
+ await checkpoint_if_cancelled()
+
+ self._element_yielded = True
+ value = cast(T, self._link.value)
+ next_link = self._link.next
+ assert next_link is not None
+ self._link = next_link
+ if not had_yieldpoint:
+ await cancel_shielded_checkpoint()
+
+ return value
+
+
+async def _operator_add(x: T, y: T) -> T:
+ return operator.add(x, y)
+
+
+async def accumulate(
+ iterable: Iterable[T] | AsyncIterable[T],
+ function: Callable[[T, T], Awaitable[T]] = _operator_add,
+ *,
+ initial: T | None = None,
+) -> AsyncGenerator[T, None]:
+ iterator = _iterate(iterable)
+ if initial is None:
+ try:
+ total = await anext(iterator)
+ except StopAsyncIteration:
+ await checkpoint()
+ return
+ else:
+ await checkpoint_if_cancelled()
+ total = initial
+ await cancel_shielded_checkpoint()
+
+ yield total
+
+ async for element in iterator:
+ total = await function(total, element)
+ yield total
+
+
+async def batched(
+ iterable: Iterable[T] | AsyncIterable[T], n: int, *, strict: bool = False
+) -> AsyncGenerator[tuple[T, ...], None]:
+ if n < 1:
+ raise ValueError("n must be at least one")
+
+ iterator = _iterate(iterable)
+
+ while True:
+ batch: list[T] = []
+ for _ in range(n):
+ try:
+ batch.append(await anext(iterator))
+ except StopAsyncIteration:
+ if not batch:
+ await checkpoint()
+ return
+ if strict:
+ raise ValueError("batched(): incomplete batch") from None
+
+ yield tuple(batch)
+ return
+
+ yield tuple(batch)
+
+
+class Chain:
+ def __call__(
+ self, *iterables: Iterable[T] | AsyncIterable[T]
+ ) -> AsyncGenerator[T, None]:
+ return self.from_iterable(iterables)
+
+ async def from_iterable(
+ self,
+ iterables: (
+ Iterable[Iterable[T] | AsyncIterable[T]]
+ | AsyncIterable[Iterable[T] | AsyncIterable[T]]
+ ),
+ ) -> AsyncGenerator[T, None]:
+ element_yielded = False
+ outer_iter = _iterate(iterables)
+
+ try:
+ async for iterable in outer_iter:
+ async for element in _iterate(iterable):
+ element_yielded = True
+ yield element
+ finally:
+ aclose = getattr(outer_iter, "aclose", None)
+ if aclose is not None:
+ with CancelScope(shield=True):
+ await aclose()
+
+ if not element_yielded:
+ await checkpoint()
+
+
+chain: Chain = Chain()
+
+
+async def combinations(
+ iterable: Iterable[T] | AsyncIterable[T], r: int
+) -> AsyncGenerator[tuple[T, ...], None]:
+ pool: list[T] = [element async for element in _iterate(iterable)]
+ async for combination in _iterate(itertools.combinations(pool, r)):
+ yield combination
+
+
+async def combinations_with_replacement(
+ iterable: Iterable[T] | AsyncIterable[T], r: int
+) -> AsyncGenerator[tuple[T, ...], None]:
+ pool: list[T] = [element async for element in _iterate(iterable)]
+ async for combination in _iterate(itertools.combinations_with_replacement(pool, r)):
+ yield combination
+
+
+async def compress(
+ data: Iterable[T] | AsyncIterable[T],
+ selectors: Iterable[object] | AsyncIterable[object],
+) -> AsyncGenerator[T, None]:
+ data_iterator = _iterate(data)
+ selector_iterator = _iterate(selectors)
+ element_yielded = False
+
+ while True:
+ try:
+ datum = await anext(data_iterator)
+ selector = await anext(selector_iterator)
+ except StopAsyncIteration:
+ if not element_yielded:
+ await checkpoint()
+
+ return
+
+ if selector:
+ element_yielded = True
+ yield datum
+
+
+async def count(start: int = 0, step: int = 1) -> AsyncGenerator[int, None]:
+ n = start
+ while True:
+ await checkpoint_if_cancelled()
+ value = n
+ n += step
+ await cancel_shielded_checkpoint()
+ yield value
+
+
+async def cycle(
+ iterable: Iterable[T] | AsyncIterable[T],
+) -> AsyncGenerator[T, None]:
+ saved: list[T] = []
+ async for element in _iterate(iterable):
+ saved.append(element)
+ yield element
+
+ if not saved:
+ await checkpoint()
+ return
+
+ while True:
+ for element in saved:
+ await checkpoint()
+ yield element
+
+
+async def dropwhile(
+ predicate: Callable[[T], Awaitable[object]],
+ iterable: Iterable[T] | AsyncIterable[T],
+) -> AsyncGenerator[T, None]:
+ element_yielded = False
+ dropping = True
+
+ async for element in _iterate(iterable):
+ if dropping and await predicate(element):
+ continue
+
+ dropping = False
+ element_yielded = True
+ yield element
+
+ if not element_yielded:
+ await checkpoint()
+
+
+async def filterfalse(
+ predicate: Callable[[T], Awaitable[object]],
+ iterable: Iterable[T] | AsyncIterable[T],
+) -> AsyncGenerator[T, None]:
+ element_yielded = False
+
+ async for element in _iterate(iterable):
+ if not await predicate(element):
+ element_yielded = True
+ yield element
+
+ if not element_yielded:
+ await checkpoint()
+
+
+@overload
+def groupby(
+ iterable: Iterable[T] | AsyncIterable[T],
+) -> AsyncGenerator[tuple[T, list[T]], None]: ...
+
+
+@overload
+def groupby(
+ iterable: Iterable[T] | AsyncIterable[T],
+ key: Callable[[T], Awaitable[R]],
+) -> AsyncGenerator[tuple[R, list[T]], None]: ...
+
+
+async def groupby(
+ iterable: Iterable[T] | AsyncIterable[T],
+ key: Callable[[T], Awaitable[object]] | None = None,
+) -> AsyncGenerator[tuple[object, list[T]], None]:
+ iterator = _iterate(iterable)
+ try:
+ element = await anext(iterator)
+ except StopAsyncIteration:
+ await checkpoint()
+ return
+
+ group_key = element if key is None else await key(element)
+ values = [element]
+
+ async for element in iterator:
+ next_key = element if key is None else await key(element)
+ if next_key != group_key:
+ completed_group = group_key, values
+ group_key = next_key
+ values = [element]
+ yield completed_group
+ else:
+ values.append(element)
+
+ yield group_key, values
+
+
+@overload
+def islice(
+ iterable: Iterable[T] | AsyncIterable[T],
+ stop: int | None,
+ /,
+) -> AsyncGenerator[T, None]: ...
+
+
+@overload
+def islice(
+ iterable: Iterable[T] | AsyncIterable[T],
+ start: int | None,
+ stop: int | None,
+ step: int | None = 1,
+ /,
+) -> AsyncGenerator[T, None]: ...
+
+
+async def islice(
+ iterable: Iterable[T] | AsyncIterable[T],
+ *args: int | None,
+) -> AsyncGenerator[T, None]:
+ if not args:
+ raise TypeError("islice expected at least 2 arguments, got 1")
+ if len(args) > 3:
+ raise TypeError(f"islice expected at most 4 arguments, got {len(args) + 1}")
+
+ slice_args = slice(*args)
+
+ start_message = (
+ "Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize."
+ )
+ stop_message = (
+ "Stop argument for islice() must be None or an integer: 0 <= x <= sys.maxsize."
+ )
+ step_message = "Step for islice() must be a positive integer or None."
+
+ def normalize_index(value: object, message: str) -> int:
+ try:
+ index = operator.index(cast(Any, value))
+ except TypeError:
+ raise ValueError(message) from None
+
+ if index < 0 or index > sys.maxsize:
+ raise ValueError(message)
+
+ return index
+
+ start = (
+ 0
+ if slice_args.start is None
+ else normalize_index(slice_args.start, start_message)
+ )
+ stop = (
+ None
+ if slice_args.stop is None
+ else normalize_index(slice_args.stop, stop_message)
+ )
+ step = (
+ 1 if slice_args.step is None else normalize_index(slice_args.step, step_message)
+ )
+
+ if step <= 0:
+ raise ValueError(step_message)
+
+ if stop == 0 or start == stop:
+ await checkpoint()
+ return
+
+ iterator = _iterate(iterable)
+ index = 0
+ element_yielded = False
+
+ while stop is None or index < stop:
+ try:
+ element = await anext(iterator)
+ except StopAsyncIteration:
+ if not element_yielded:
+ await checkpoint()
+
+ return
+
+ if index >= start and (index - start) % step == 0:
+ index += 1
+ element_yielded = True
+ yield element
+ else:
+ index += 1
+
+ if not element_yielded:
+ await checkpoint()
+
+
+async def pairwise(
+ iterable: Iterable[T] | AsyncIterable[T],
+) -> AsyncGenerator[tuple[T, T], None]:
+ iterator = _iterate(iterable)
+ try:
+ previous = await anext(iterator)
+ except StopAsyncIteration:
+ await checkpoint()
+ return
+
+ element_yielded = False
+ async for element in iterator:
+ element_yielded = True
+ pair = (previous, element)
+ previous = element
+ yield pair
+
+ if not element_yielded:
+ await checkpoint()
+
+
+async def permutations(
+ iterable: Iterable[T] | AsyncIterable[T], r: int | None = None
+) -> AsyncGenerator[tuple[T, ...], None]:
+ pool: list[T] = [element async for element in _iterate(iterable)]
+ n = len(pool)
+ if r is None:
+ r = n
+ elif not isinstance(r, int):
+ raise TypeError("Expected int as r")
+ elif r < 0:
+ raise ValueError("r must be non-negative")
+
+ async for permutation in _iterate(itertools.permutations(pool, r)):
+ yield permutation
+
+
+async def product(
+ *iterables: Iterable[T] | AsyncIterable[T], repeat: int = 1
+) -> AsyncGenerator[tuple[T, ...], None]:
+ repeat = operator.index(repeat)
+ if repeat < 0:
+ raise ValueError("repeat argument cannot be negative")
+
+ pools: list[tuple[T, ...]] = []
+ for iterable in iterables:
+ pool: list[T] = [element async for element in _iterate(iterable)]
+ pools.append(tuple(pool))
+
+ async for value in _iterate(itertools.product(*pools, repeat=repeat)):
+ yield value
+
+
+async def repeat(element: T, times: int | None = None) -> AsyncGenerator[T, None]:
+ if times is None:
+ while True:
+ await checkpoint()
+ yield element
+
+ remaining = operator.index(cast(Any, times))
+ if remaining <= 0:
+ await checkpoint()
+ return
+
+ while remaining > 0:
+ await checkpoint_if_cancelled()
+ remaining -= 1
+ await cancel_shielded_checkpoint()
+ yield element
+
+
+async def starmap(
+ function: Callable[..., Awaitable[R]],
+ iterable: (
+ Iterable[Iterable[object] | AsyncIterable[object]]
+ | AsyncIterable[Iterable[object] | AsyncIterable[object]]
+ ),
+) -> AsyncGenerator[R, None]:
+ result_yielded = False
+
+ async for args_iterable in _iterate(iterable):
+ args = [element async for element in _iterate(args_iterable)]
+ result_yielded = True
+ yield await function(*args)
+
+ if not result_yielded:
+ await checkpoint()
+
+
+def tee(
+ iterable: Iterable[T] | AsyncIterable[T], n: int = 2
+) -> tuple[AsyncIterator[T], ...]:
+ n = operator.index(cast(Any, n))
+ if n < 0:
+ raise ValueError("n must be >= 0")
+ if n == 0:
+ return ()
+
+ iterator = _TeeAsyncIterator(iterable)
+ iterators: list[AsyncIterator[T]] = [iterator]
+ iterators.extend(_TeeAsyncIterator(iterator) for _ in range(n - 1))
+ return tuple(iterators)
+
+
+async def takewhile(
+ predicate: Callable[[T], Awaitable[object]],
+ iterable: Iterable[T] | AsyncIterable[T],
+) -> AsyncGenerator[T, None]:
+ element_yielded = False
+
+ async for element in _iterate(iterable):
+ if not await predicate(element):
+ if not element_yielded:
+ await checkpoint()
+
+ return
+
+ element_yielded = True
+ yield element
+
+ if not element_yielded:
+ await checkpoint()
+
+
+async def zip_longest(
+ *iterables: Iterable[object] | AsyncIterable[object],
+ fillvalue: object = None,
+) -> AsyncGenerator[tuple[object, ...], None]:
+ iterators = [_iterate(iterable) for iterable in iterables]
+ num_active = len(iterators)
+ if not num_active:
+ await checkpoint()
+ return
+
+ active = [True] * num_active
+ tuple_yielded = False
+
+ while True:
+ values: list[object] = []
+ for index, iterator in enumerate(iterators):
+ if not active[index]:
+ values.append(fillvalue)
+ continue
+
+ try:
+ value = await anext(iterator)
+ except StopAsyncIteration:
+ active[index] = False
+ num_active -= 1
+ if not num_active:
+ if not tuple_yielded:
+ await checkpoint()
+
+ return
+
+ value = fillvalue
+
+ values.append(value)
+
+ tuple_yielded = True
+ yield tuple(values)
diff --git a/venv/lib/python3.11/site-packages/anyio/lowlevel.py b/venv/lib/python3.11/site-packages/anyio/lowlevel.py
new file mode 100644
index 000000000..361b2eb84
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/lowlevel.py
@@ -0,0 +1,228 @@
+from __future__ import annotations
+
+__all__ = (
+ "EventLoopToken",
+ "RunVar",
+ "RunvarToken",
+ "cancel_shielded_checkpoint",
+ "checkpoint",
+ "checkpoint_if_cancelled",
+ "current_token",
+)
+
+import enum
+from dataclasses import dataclass
+from types import TracebackType
+from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, final, overload
+from weakref import WeakKeyDictionary
+
+from ._core._eventloop import get_async_backend
+
+if TYPE_CHECKING:
+ from .abc import AsyncBackend
+
+T = TypeVar("T")
+D = TypeVar("D")
+
+
+async def checkpoint() -> None:
+ """
+ Check for cancellation and allow the scheduler to switch to another task.
+
+ Equivalent to (but more efficient than)::
+
+ await checkpoint_if_cancelled()
+ await cancel_shielded_checkpoint()
+
+ .. versionadded:: 3.0
+
+ """
+ await get_async_backend().checkpoint()
+
+
+async def checkpoint_if_cancelled() -> None:
+ """
+ Enter a checkpoint if the enclosing cancel scope has been cancelled.
+
+ This does not allow the scheduler to switch to a different task.
+
+ .. versionadded:: 3.0
+
+ """
+ await get_async_backend().checkpoint_if_cancelled()
+
+
+async def cancel_shielded_checkpoint() -> None:
+ """
+ Allow the scheduler to switch to another task but without checking for cancellation.
+
+ Equivalent to (but potentially more efficient than)::
+
+ with CancelScope(shield=True):
+ await checkpoint()
+
+ .. versionadded:: 3.0
+
+ """
+ await get_async_backend().cancel_shielded_checkpoint()
+
+
+@final
+@dataclass(frozen=True, repr=False)
+class EventLoopToken:
+ """
+ An opaque object that holds a reference to an event loop.
+
+ .. versionadded:: 4.11.0
+ """
+
+ backend_class: type[AsyncBackend]
+ native_token: object
+
+
+def current_token() -> EventLoopToken:
+ """
+ Return a token object that can be used to call code in the current event loop from
+ another thread.
+
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ .. versionadded:: 4.11.0
+
+ """
+ backend_class = get_async_backend()
+ raw_token = backend_class.current_token()
+ return EventLoopToken(backend_class, raw_token)
+
+
+_run_vars: WeakKeyDictionary[object, dict[RunVar[Any], Any]] = WeakKeyDictionary()
+
+
+class _NoValueSet(enum.Enum):
+ NO_VALUE_SET = enum.auto()
+
+
+class RunvarToken(Generic[T]):
+ """
+ A token that can be used to restore a :class:`RunVar` to its previous value.
+
+ Returned by :meth:`RunVar.set`. Can be used as a context manager to automatically
+ reset the variable on exit, or passed directly to :meth:`RunVar.reset`.
+ """
+
+ __slots__ = "_redeemed", "_value", "_var"
+
+ def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]):
+ self._var = var
+ self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value
+ self._redeemed = False
+
+ def __enter__(self) -> RunvarToken[T]:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self._var.reset(self)
+
+
+class RunVar(Generic[T]):
+ """
+ Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop.
+
+ Can be used as a context manager, Just like :class:`~contextvars.ContextVar`, that
+ will reset the variable to its previous value when the context block is exited.
+ """
+
+ __slots__ = "_default", "_name"
+
+ NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET
+
+ def __init__(
+ self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET
+ ):
+ self._name = name
+ self._default = default
+
+ @property
+ def _current_vars(self) -> dict[RunVar[T], T]:
+ native_token = current_token().native_token
+ try:
+ return _run_vars[native_token]
+ except KeyError:
+ run_vars = _run_vars[native_token] = {}
+ return run_vars
+
+ @overload
+ def get(self, default: D) -> T | D: ...
+
+ @overload
+ def get(self) -> T: ...
+
+ def get(
+ self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET
+ ) -> T | D:
+ """
+ Return the current value of this run variable.
+
+ :param default: a fallback value to return if no value has been set
+ :return: the current value, the provided default, or the variable's own default
+ :raises LookupError: if no value is set and no default is available
+
+ """
+ try:
+ return self._current_vars[self]
+ except KeyError:
+ if default is not RunVar.NO_VALUE_SET:
+ return default
+ elif self._default is not RunVar.NO_VALUE_SET:
+ return self._default
+
+ raise LookupError(
+ f'Run variable "{self._name}" has no value and no default set'
+ )
+
+ def set(self, value: T) -> RunvarToken[T]:
+ """
+ Set the value of this run variable for the current event loop.
+
+ :param value: the new value
+ :return: a token that can be used to restore the previous value
+
+ """
+ current_vars = self._current_vars
+ token = RunvarToken(self, current_vars.get(self, RunVar.NO_VALUE_SET))
+ current_vars[self] = value
+ return token
+
+ def reset(self, token: RunvarToken[T]) -> None:
+ """
+ Restore this run variable to the value it held before the matching :meth:`set`.
+
+ :param token: the token returned by :meth:`set`
+ :raises ValueError: if the token belongs to a different :class:`RunVar` or the token
+ has already been used
+
+ """
+ if token._var is not self:
+ raise ValueError("This token does not belong to this RunVar")
+
+ if token._redeemed:
+ raise ValueError("This token has already been used")
+
+ if token._value is _NoValueSet.NO_VALUE_SET:
+ try:
+ del self._current_vars[self]
+ except KeyError:
+ pass
+ else:
+ self._current_vars[self] = token._value
+
+ token._redeemed = True
+
+ def __repr__(self) -> str:
+ return f""
diff --git a/venv/lib/python3.11/site-packages/anyio/py.typed b/venv/lib/python3.11/site-packages/anyio/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/anyio/pytest_plugin.py b/venv/lib/python3.11/site-packages/anyio/pytest_plugin.py
new file mode 100644
index 000000000..d6238cd11
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/pytest_plugin.py
@@ -0,0 +1,404 @@
+from __future__ import annotations
+
+import dataclasses
+import socket
+import sys
+from collections.abc import Callable, Generator, Iterator
+from contextlib import ExitStack, contextmanager
+from inspect import isasyncgenfunction, iscoroutinefunction, ismethod
+from typing import TYPE_CHECKING, Any, cast
+
+import pytest
+from _pytest.fixtures import FuncFixtureInfo, SubRequest
+from _pytest.outcomes import Exit
+from _pytest.scope import Scope
+
+from . import get_available_backends
+from ._core._eventloop import (
+ current_async_library,
+ get_async_backend,
+ reset_current_async_library,
+ set_current_async_library,
+)
+from ._core._exceptions import iterate_exceptions
+
+if TYPE_CHECKING:
+ from .abc import TestRunner
+
+if sys.version_info < (3, 11):
+ from exceptiongroup import ExceptionGroup
+
+if TYPE_CHECKING:
+ # pytest >= 9.2 keeps CallSpec2 as a TYPE_CHECKING-only alias of CallSpec
+ from _pytest.python import CallSpec2 as CallSpec
+else:
+ try:
+ # c.f. https://github.com/pytest-dev/pytest/pull/14742
+ # pytest >= 9.2
+ from _pytest.python import CallSpec
+ except ImportError:
+ from _pytest.python import CallSpec2 as CallSpec
+
+_current_runner: TestRunner | None = None
+_runner_stack: ExitStack | None = None
+_runner_leases = 0
+
+
+def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]:
+ if isinstance(backend, str):
+ return backend, {}
+ elif isinstance(backend, tuple) and len(backend) == 2:
+ if isinstance(backend[0], str) and isinstance(backend[1], dict):
+ return cast(tuple[str, dict[str, Any]], backend)
+
+ raise TypeError("anyio_backend must be either a string or tuple of (string, dict)")
+
+
+@contextmanager
+def get_runner(
+ backend_name: str, backend_options: dict[str, Any]
+) -> Iterator[TestRunner]:
+ global _current_runner, _runner_leases, _runner_stack
+ if _current_runner is None:
+ asynclib = get_async_backend(backend_name)
+ _runner_stack = ExitStack()
+ if current_async_library() is None:
+ # Since we're in control of the event loop, we can cache the name of the
+ # async library
+ token = set_current_async_library(backend_name)
+ _runner_stack.callback(reset_current_async_library, token)
+
+ backend_options = backend_options or {}
+ _current_runner = _runner_stack.enter_context(
+ asynclib.create_test_runner(backend_options)
+ )
+
+ _runner_leases += 1
+ try:
+ yield _current_runner
+ finally:
+ _runner_leases -= 1
+ if not _runner_leases:
+ assert _runner_stack is not None
+ _runner_stack.close()
+ _runner_stack = _current_runner = None
+
+
+def pytest_addoption(parser: pytest.Parser) -> None:
+ group = parser.getgroup("anyio")
+ parser.addini(
+ "anyio_mode",
+ default="strict",
+ help='AnyIO plugin mode (either "strict" or "auto")',
+ )
+ group.addoption(
+ "--anyio-mode",
+ default=None,
+ dest="anyio_mode",
+ help="""
+ 'auto' - All async test functions will be handled by AnyIO pytest plugin
+ 'strict' - Disabling autoprocessing(useful when anyio tests
+ need to coexist with other async test plugins)
+ """,
+ )
+
+
+def _is_auto_mode(config: pytest.Config, mode: str) -> bool:
+ return (
+ config.getoption(mode, default=None) == "auto" or config.getini(mode) == "auto"
+ )
+
+
+def pytest_configure(config: pytest.Config) -> None:
+ config.addinivalue_line(
+ "markers",
+ "anyio: mark the (coroutine function) test to be run asynchronously via anyio.",
+ )
+ if (
+ _is_auto_mode(config, "anyio_mode")
+ and config.pluginmanager.has_plugin("asyncio")
+ and _is_auto_mode(config, "asyncio_mode")
+ ):
+ config.issue_config_time_warning(
+ pytest.PytestConfigWarning(
+ "AnyIO auto mode has been enabled together with pytest-asyncio auto "
+ "mode. This may cause unexpected behavior."
+ ),
+ 1,
+ )
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]:
+ def wrapper(anyio_backend: Any, request: SubRequest, **kwargs: Any) -> Any:
+ # Rebind any fixture methods to the request instance
+ if (
+ request.instance
+ and ismethod(func)
+ and type(func.__self__) is type(request.instance)
+ ):
+ local_func = func.__func__.__get__(request.instance)
+ else:
+ local_func = func
+
+ backend_name, backend_options = extract_backend_and_options(anyio_backend)
+ if has_backend_arg:
+ kwargs["anyio_backend"] = anyio_backend
+
+ if has_request_arg:
+ kwargs["request"] = request
+
+ with get_runner(backend_name, backend_options) as runner:
+ # re-entrant call into the test runner detected. this happens when an async fixture
+ # is dynamically requested via request.getfixturevalue() from inside a running async
+ # test or fixture. on asyncio this raises RuntimeError: This event loop is already
+ # running, on trio the runner deadlocks - the host loop blocks waiting for the
+ # coroutine to return, but the coroutine is waiting for the host loop. raising here
+ # prevents the hang and gives a consistent error across backends.
+ if runner.is_running():
+ raise RuntimeError(
+ "Cannot schedule a coroutine in the test runner while another is already running; "
+ "likely caused by request.getfixturevalue() on an async fixture."
+ )
+
+ if isasyncgenfunction(local_func):
+ yield from runner.run_asyncgen_fixture(local_func, kwargs)
+ else:
+ yield runner.run_fixture(local_func, kwargs)
+
+ # Only apply this to coroutine functions and async generator functions in requests
+ # that involve the anyio_backend fixture
+ func = fixturedef.func
+ if isasyncgenfunction(func) or iscoroutinefunction(func):
+ if "anyio_backend" in request.fixturenames:
+ fixturedef.func = wrapper
+ original_argname = fixturedef.argnames
+
+ if not (has_backend_arg := "anyio_backend" in fixturedef.argnames):
+ fixturedef.argnames += ("anyio_backend",)
+
+ if not (has_request_arg := "request" in fixturedef.argnames):
+ fixturedef.argnames += ("request",)
+
+ try:
+ return (yield)
+ finally:
+ fixturedef.func = func
+ fixturedef.argnames = original_argname
+
+ return (yield)
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_pycollect_makeitem(
+ collector: pytest.Module | pytest.Class, name: str, obj: object
+) -> None:
+ if collector.istestfunction(obj, name):
+ inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj
+ if iscoroutinefunction(inner_func):
+ anyio_auto_mode = _is_auto_mode(collector.config, "anyio_mode")
+ marker = collector.get_closest_marker("anyio")
+ own_markers = getattr(obj, "pytestmark", ())
+ if (
+ anyio_auto_mode
+ or marker
+ or any(marker.name == "anyio" for marker in own_markers)
+ ):
+ pytest.mark.usefixtures("anyio_backend")(obj)
+
+
+def pytest_collection_finish(session: pytest.Session) -> None:
+ for i, item in reversed(list(enumerate(session.items))):
+ if (
+ isinstance(item, pytest.Function)
+ and iscoroutinefunction(item.function)
+ and item.get_closest_marker("anyio") is not None
+ and "anyio_backend" not in item.fixturenames
+ ):
+ new_items = []
+ try:
+ cs_fields = {f.name for f in dataclasses.fields(CallSpec)}
+ except TypeError:
+ cs_fields = set()
+
+ for param_index, backend in enumerate(get_available_backends()):
+ if "_arg2scope" in cs_fields: # pytest >= 8
+ callspec = CallSpec(
+ params={"anyio_backend": backend},
+ indices={"anyio_backend": param_index},
+ _arg2scope={"anyio_backend": Scope.Module},
+ _idlist=[backend],
+ marks=[],
+ )
+ else: # pytest 7.x
+ callspec = CallSpec( # type: ignore[call-arg]
+ funcargs={},
+ params={"anyio_backend": backend},
+ indices={"anyio_backend": param_index},
+ arg2scope={"anyio_backend": Scope.Module},
+ idlist=[backend],
+ marks=[],
+ )
+
+ fi = item._fixtureinfo
+ new_names_closure = list(fi.names_closure)
+ if "anyio_backend" not in new_names_closure:
+ new_names_closure.append("anyio_backend")
+
+ new_fixtureinfo = FuncFixtureInfo(
+ argnames=fi.argnames,
+ initialnames=fi.initialnames,
+ names_closure=new_names_closure,
+ name2fixturedefs=fi.name2fixturedefs,
+ )
+ new_item = pytest.Function.from_parent(
+ item.parent,
+ name=f"{item.originalname}[{backend}]",
+ callspec=callspec,
+ callobj=item.obj,
+ fixtureinfo=new_fixtureinfo,
+ keywords=item.keywords,
+ originalname=item.originalname,
+ )
+ new_items.append(new_item)
+
+ session.items[i : i + 1] = new_items
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None:
+ def run_with_hypothesis(**kwargs: Any) -> None:
+ with get_runner(backend_name, backend_options) as runner:
+ runner.run_test(original_func, kwargs)
+
+ backend = pyfuncitem.funcargs.get("anyio_backend")
+ if backend:
+ backend_name, backend_options = extract_backend_and_options(backend)
+
+ if hasattr(pyfuncitem.obj, "hypothesis"):
+ # Wrap the inner test function unless it's already wrapped
+ original_func = pyfuncitem.obj.hypothesis.inner_test
+ if original_func.__qualname__ != run_with_hypothesis.__qualname__:
+ if iscoroutinefunction(original_func):
+ pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis
+
+ return None
+
+ if iscoroutinefunction(pyfuncitem.obj):
+ funcargs = pyfuncitem.funcargs
+ testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
+ with get_runner(backend_name, backend_options) as runner:
+ try:
+ runner.run_test(pyfuncitem.obj, testargs)
+ except ExceptionGroup as excgrp:
+ for exc in iterate_exceptions(excgrp):
+ if isinstance(exc, (Exit, KeyboardInterrupt, SystemExit)):
+ raise exc from excgrp
+
+ raise
+
+ return True
+
+ return None
+
+
+@pytest.fixture(scope="module", params=get_available_backends())
+def anyio_backend(request: Any) -> Any:
+ return request.param
+
+
+@pytest.fixture
+def anyio_backend_name(anyio_backend: Any) -> str:
+ if isinstance(anyio_backend, str):
+ return anyio_backend
+ else:
+ return anyio_backend[0]
+
+
+@pytest.fixture
+def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]:
+ if isinstance(anyio_backend, str):
+ return {}
+ else:
+ return anyio_backend[1]
+
+
+class FreePortFactory:
+ """
+ Manages port generation based on specified socket kind, ensuring no duplicate
+ ports are generated.
+
+ This class provides functionality for generating available free ports on the
+ system. It is initialized with a specific socket kind and can generate ports
+ for given address families while avoiding reuse of previously generated ports.
+
+ Users should not instantiate this class directly, but use the
+ ``free_tcp_port_factory`` and ``free_udp_port_factory`` fixtures instead. For simple
+ uses cases, ``free_tcp_port`` and ``free_udp_port`` can be used instead.
+ """
+
+ def __init__(self, kind: socket.SocketKind) -> None:
+ self._kind = kind
+ self._generated = set[int]()
+
+ @property
+ def kind(self) -> socket.SocketKind:
+ """
+ The type of socket connection (e.g., :data:`~socket.SOCK_STREAM` or
+ :data:`~socket.SOCK_DGRAM`) used to bind for checking port availability
+
+ """
+ return self._kind
+
+ def __call__(self, family: socket.AddressFamily | None = None) -> int:
+ """
+ Return an unbound port for the given address family.
+
+ :param family: if omitted, both IPv4 and IPv6 addresses will be tried
+ :return: a port number
+
+ """
+ if family is not None:
+ families = [family]
+ else:
+ families = [socket.AF_INET]
+ if socket.has_ipv6:
+ families.append(socket.AF_INET6)
+
+ while True:
+ port = 0
+ with ExitStack() as stack:
+ for fam in families:
+ sock = stack.enter_context(socket.socket(fam, self._kind))
+ addr = "::1" if fam == socket.AF_INET6 else "127.0.0.1"
+ try:
+ sock.bind((addr, port))
+ except OSError:
+ break
+
+ if not port:
+ port = sock.getsockname()[1]
+ else:
+ if port not in self._generated:
+ self._generated.add(port)
+ return port
+
+
+@pytest.fixture(scope="session")
+def free_tcp_port_factory() -> FreePortFactory:
+ return FreePortFactory(socket.SOCK_STREAM)
+
+
+@pytest.fixture(scope="session")
+def free_udp_port_factory() -> FreePortFactory:
+ return FreePortFactory(socket.SOCK_DGRAM)
+
+
+@pytest.fixture
+def free_tcp_port(free_tcp_port_factory: Callable[[], int]) -> int:
+ return free_tcp_port_factory()
+
+
+@pytest.fixture
+def free_udp_port(free_udp_port_factory: Callable[[], int]) -> int:
+ return free_udp_port_factory()
diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__init__.py b/venv/lib/python3.11/site-packages/anyio/streams/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/venv/lib/python3.11/site-packages/anyio/streams/buffered.py b/venv/lib/python3.11/site-packages/anyio/streams/buffered.py
new file mode 100644
index 000000000..a3b07f73a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/streams/buffered.py
@@ -0,0 +1,201 @@
+from __future__ import annotations
+
+__all__ = (
+ "BufferedByteReceiveStream",
+ "BufferedByteStream",
+ "BufferedConnectable",
+)
+
+import sys
+from collections.abc import Callable, Iterable, Mapping
+from dataclasses import dataclass, field
+from typing import Any, SupportsIndex
+
+from .. import ClosedResourceError, DelimiterNotFound, EndOfStream, IncompleteRead
+from ..abc import (
+ AnyByteReceiveStream,
+ AnyByteStream,
+ AnyByteStreamConnectable,
+ ByteReceiveStream,
+ ByteStream,
+ ByteStreamConnectable,
+)
+
+if sys.version_info >= (3, 12):
+ from typing import override
+else:
+ from typing_extensions import override
+
+
+@dataclass(eq=False)
+class BufferedByteReceiveStream(ByteReceiveStream):
+ """
+ Wraps any bytes-based receive stream and uses a buffer to provide sophisticated
+ receiving capabilities in the form of a byte stream.
+ """
+
+ receive_stream: AnyByteReceiveStream
+ _buffer: bytearray = field(init=False, default_factory=bytearray)
+ _closed: bool = field(init=False, default=False)
+
+ async def aclose(self) -> None:
+ await self.receive_stream.aclose()
+ self._closed = True
+
+ @property
+ def buffer(self) -> bytes:
+ """The bytes currently in the buffer."""
+ return bytes(self._buffer)
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return self.receive_stream.extra_attributes
+
+ def feed_data(self, data: Iterable[SupportsIndex], /) -> None:
+ """
+ Append data directly into the buffer.
+
+ Any data in the buffer will be consumed by receive operations before receiving
+ anything from the wrapped stream.
+
+ :param data: the data to append to the buffer (can be bytes or anything else
+ that supports ``__index__()``)
+
+ """
+ self._buffer.extend(data)
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ if self._closed:
+ raise ClosedResourceError
+
+ if self._buffer:
+ chunk = bytes(self._buffer[:max_bytes])
+ del self._buffer[:max_bytes]
+ return chunk
+ elif isinstance(self.receive_stream, ByteReceiveStream):
+ return await self.receive_stream.receive(max_bytes)
+ else:
+ # With a bytes-oriented object stream, we need to handle any surplus bytes
+ # we get from the receive() call
+ chunk = await self.receive_stream.receive()
+ if len(chunk) > max_bytes:
+ # Save the surplus bytes in the buffer
+ self._buffer.extend(chunk[max_bytes:])
+ return chunk[:max_bytes]
+ else:
+ return chunk
+
+ async def receive_exactly(self, nbytes: int) -> bytes:
+ """
+ Read exactly the given amount of bytes from the stream.
+
+ :param nbytes: the number of bytes to read
+ :return: the bytes read
+ :raises ~anyio.IncompleteRead: if the stream was closed before the requested
+ amount of bytes could be read from the stream
+
+ """
+ while True:
+ remaining = nbytes - len(self._buffer)
+ if remaining <= 0:
+ retval = self._buffer[:nbytes]
+ del self._buffer[:nbytes]
+ return bytes(retval)
+
+ try:
+ if isinstance(self.receive_stream, ByteReceiveStream):
+ chunk = await self.receive_stream.receive(remaining)
+ else:
+ chunk = await self.receive_stream.receive()
+ except EndOfStream as exc:
+ raise IncompleteRead from exc
+
+ self._buffer.extend(chunk)
+
+ async def receive_until(self, delimiter: bytes, max_bytes: int) -> bytes:
+ """
+ Read from the stream until the delimiter is found or max_bytes have been read.
+
+ :param delimiter: the marker to look for in the stream
+ :param max_bytes: maximum number of bytes that will be read before raising
+ :exc:`~anyio.DelimiterNotFound`
+ :return: the bytes read (not including the delimiter)
+ :raises ~anyio.IncompleteRead: if the stream was closed before the delimiter
+ was found
+ :raises ~anyio.DelimiterNotFound: if the delimiter is not found within the
+ bytes read up to the maximum allowed
+
+ """
+ delimiter_size = len(delimiter)
+ offset = 0
+ while True:
+ # Check if the delimiter can be found in the current buffer
+ index = self._buffer.find(delimiter, offset)
+ if index >= 0:
+ found = self._buffer[:index]
+ del self._buffer[: index + len(delimiter) :]
+ return bytes(found)
+
+ # Check if the buffer is already at or over the limit
+ if len(self._buffer) >= max_bytes:
+ raise DelimiterNotFound(max_bytes)
+
+ # Read more data into the buffer from the socket
+ try:
+ data = await self.receive_stream.receive()
+ except EndOfStream as exc:
+ raise IncompleteRead from exc
+
+ # Move the offset forward and add the new data to the buffer
+ offset = max(len(self._buffer) - delimiter_size + 1, 0)
+ self._buffer.extend(data)
+
+
+class BufferedByteStream(BufferedByteReceiveStream, ByteStream):
+ """
+ A full-duplex variant of :class:`BufferedByteReceiveStream`. All writes are passed
+ through to the wrapped stream as-is.
+ """
+
+ def __init__(self, stream: AnyByteStream):
+ """
+ :param stream: the stream to be wrapped
+
+ """
+ super().__init__(stream)
+ self._stream = stream
+
+ @override
+ async def send_eof(self) -> None:
+ await self._stream.send_eof()
+
+ @override
+ async def send(self, item: bytes) -> None:
+ await self._stream.send(item)
+
+
+class BufferedConnectable(ByteStreamConnectable):
+ """
+ Wraps a byte stream connectable to produce :class:`BufferedByteStream` connections.
+
+ Use this when you want the streams returned by :meth:`connect` to have the buffered
+ receive API (e.g. :meth:`~BufferedByteReceiveStream.receive_exactly` and
+ :meth:`~BufferedByteReceiveStream.receive_until`).
+
+ :param connectable: the byte stream connectable to wrap
+ """
+
+ def __init__(self, connectable: AnyByteStreamConnectable):
+ """
+ :param connectable: the connectable to wrap
+
+ """
+ self.connectable = connectable
+
+ @override
+ async def connect(self) -> BufferedByteStream:
+ stream = await self.connectable.connect()
+ return BufferedByteStream(stream)
diff --git a/venv/lib/python3.11/site-packages/anyio/streams/file.py b/venv/lib/python3.11/site-packages/anyio/streams/file.py
new file mode 100644
index 000000000..b9d7ca4f3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/streams/file.py
@@ -0,0 +1,157 @@
+from __future__ import annotations
+
+__all__ = (
+ "FileReadStream",
+ "FileStreamAttribute",
+ "FileWriteStream",
+)
+
+from collections.abc import Callable, Mapping
+from io import SEEK_SET, UnsupportedOperation
+from os import PathLike
+from pathlib import Path
+from typing import IO, Any
+
+from .. import (
+ BrokenResourceError,
+ ClosedResourceError,
+ EndOfStream,
+ TypedAttributeSet,
+ to_thread,
+ typed_attribute,
+)
+from ..abc import ByteReceiveStream, ByteSendStream
+
+
+class FileStreamAttribute(TypedAttributeSet):
+ #: the open file descriptor
+ file: IO[bytes] = typed_attribute()
+ #: the path of the file on the file system, if available (file must be a real file)
+ path: Path = typed_attribute()
+ #: the file number, if available (file must be a real file or a TTY)
+ fileno: int = typed_attribute()
+
+
+class _BaseFileStream:
+ def __init__(self, file: IO[bytes]):
+ self._file = file
+
+ async def aclose(self) -> None:
+ await to_thread.run_sync(self._file.close)
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ attributes: dict[Any, Callable[[], Any]] = {
+ FileStreamAttribute.file: lambda: self._file,
+ }
+
+ if hasattr(self._file, "name"):
+ attributes[FileStreamAttribute.path] = lambda: Path(self._file.name)
+
+ try:
+ self._file.fileno()
+ except UnsupportedOperation:
+ pass
+ else:
+ attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno()
+
+ return attributes
+
+
+class FileReadStream(_BaseFileStream, ByteReceiveStream):
+ """
+ A byte stream that reads from a file in the file system.
+
+ :param file: a file that has been opened for reading in binary mode
+
+ .. versionadded:: 3.0
+ """
+
+ @classmethod
+ async def from_path(cls, path: str | PathLike[str]) -> FileReadStream:
+ """
+ Create a file read stream by opening the given file.
+
+ :param path: path of the file to read from
+
+ """
+ file = await to_thread.run_sync(Path(path).open, "rb")
+ return cls(file)
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ try:
+ data = await to_thread.run_sync(self._file.read, max_bytes)
+ except ValueError:
+ raise ClosedResourceError from None
+ except OSError as exc:
+ raise BrokenResourceError from exc
+
+ if data:
+ return data
+ else:
+ raise EndOfStream
+
+ async def seek(self, position: int, whence: int = SEEK_SET) -> int:
+ """
+ Seek the file to the given position.
+
+ .. seealso:: :meth:`io.IOBase.seek`
+
+ .. note:: Not all file descriptors are seekable.
+
+ :param position: position to seek the file to
+ :param whence: controls how ``position`` is interpreted
+ :return: the new absolute position
+ :raises OSError: if the file is not seekable
+
+ """
+ return await to_thread.run_sync(self._file.seek, position, whence)
+
+ async def tell(self) -> int:
+ """
+ Return the current stream position.
+
+ .. note:: Not all file descriptors are seekable.
+
+ :return: the current absolute position
+ :raises OSError: if the file is not seekable
+
+ """
+ return await to_thread.run_sync(self._file.tell)
+
+
+class FileWriteStream(_BaseFileStream, ByteSendStream):
+ """
+ A byte stream that writes to a file in the file system.
+
+ :param file: a file that has been opened for writing in binary mode
+
+ .. versionadded:: 3.0
+ """
+
+ @classmethod
+ async def from_path(
+ cls, path: str | PathLike[str], append: bool = False
+ ) -> FileWriteStream:
+ """
+ Create a file write stream by opening the given file for writing.
+
+ :param path: path of the file to write to
+ :param append: if ``True``, open the file for appending; if ``False``, any
+ existing file at the given path will be truncated
+
+ """
+ mode = "ab" if append else "wb"
+ file = await to_thread.run_sync(Path(path).open, mode)
+ return cls(file)
+
+ async def send(self, item: bytes) -> None:
+ try:
+ await to_thread.run_sync(self._file.write, item)
+ except ValueError:
+ raise ClosedResourceError from None
+ except OSError as exc:
+ raise BrokenResourceError from exc
diff --git a/venv/lib/python3.11/site-packages/anyio/streams/memory.py b/venv/lib/python3.11/site-packages/anyio/streams/memory.py
new file mode 100644
index 000000000..329a75e44
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/streams/memory.py
@@ -0,0 +1,326 @@
+from __future__ import annotations
+
+__all__ = (
+ "MemoryObjectReceiveStream",
+ "MemoryObjectSendStream",
+ "MemoryObjectStreamStatistics",
+)
+
+import warnings
+from collections import OrderedDict, deque
+from dataclasses import dataclass, field
+from types import TracebackType
+from typing import Generic, NamedTuple, TypeVar
+
+from .._core._exceptions import (
+ BrokenResourceError,
+ ClosedResourceError,
+ EndOfStream,
+ WouldBlock,
+)
+from .._core._synchronization import Event
+from .._core._testing import TaskInfo, get_current_task
+from ..abc import ObjectReceiveStream, ObjectSendStream
+from ..lowlevel import checkpoint
+
+T_Item = TypeVar("T_Item")
+T_co = TypeVar("T_co", covariant=True)
+T_contra = TypeVar("T_contra", contravariant=True)
+
+
+class MemoryObjectStreamStatistics(NamedTuple):
+ current_buffer_used: int #: number of items stored in the buffer
+ #: maximum number of items that can be stored on this stream (or :data:`math.inf`)
+ max_buffer_size: float
+ open_send_streams: int #: number of unclosed clones of the send stream
+ open_receive_streams: int #: number of unclosed clones of the receive stream
+ #: number of tasks blocked on :meth:`MemoryObjectSendStream.send`
+ tasks_waiting_send: int
+ #: number of tasks blocked on :meth:`MemoryObjectReceiveStream.receive`
+ tasks_waiting_receive: int
+
+
+@dataclass(eq=False)
+class _MemoryObjectItemReceiver(Generic[T_Item]):
+ task_info: TaskInfo = field(init=False, default_factory=get_current_task)
+ item: T_Item = field(init=False)
+
+ def __repr__(self) -> str:
+ # When item is not defined, we get following error with default __repr__:
+ # AttributeError: 'MemoryObjectItemReceiver' object has no attribute 'item'
+ item = getattr(self, "item", None)
+ return f"{self.__class__.__name__}(task_info={self.task_info}, item={item!r})"
+
+
+@dataclass(eq=False)
+class _MemoryObjectStreamState(Generic[T_Item]):
+ max_buffer_size: float = field()
+ buffer: deque[T_Item] = field(init=False, default_factory=deque)
+ open_send_channels: int = field(init=False, default=0)
+ open_receive_channels: int = field(init=False, default=0)
+ waiting_receivers: OrderedDict[Event, _MemoryObjectItemReceiver[T_Item]] = field(
+ init=False, default_factory=OrderedDict
+ )
+ waiting_senders: OrderedDict[Event, T_Item] = field(
+ init=False, default_factory=OrderedDict
+ )
+
+ def statistics(self) -> MemoryObjectStreamStatistics:
+ return MemoryObjectStreamStatistics(
+ len(self.buffer),
+ self.max_buffer_size,
+ self.open_send_channels,
+ self.open_receive_channels,
+ len(self.waiting_senders),
+ len(self.waiting_receivers),
+ )
+
+
+@dataclass(eq=False)
+class MemoryObjectReceiveStream(ObjectReceiveStream[T_co], Generic[T_co]):
+ _state: _MemoryObjectStreamState[T_co]
+ _closed: bool = field(init=False, default=False)
+
+ def __post_init__(self) -> None:
+ self._state.open_receive_channels += 1
+
+ def receive_nowait(self) -> T_co:
+ """
+ Receive the next item if it can be done without waiting.
+
+ :return: the received item
+ :raises ~anyio.ClosedResourceError: if this send stream has been closed
+ :raises ~anyio.EndOfStream: if the buffer is empty and this stream has been
+ closed from the sending end
+ :raises ~anyio.WouldBlock: if there are no items in the buffer and no tasks
+ waiting to send
+
+ """
+ if self._closed:
+ raise ClosedResourceError
+
+ if self._state.waiting_senders:
+ # Get the item from the next sender
+ send_event, item = self._state.waiting_senders.popitem(last=False)
+ self._state.buffer.append(item)
+ send_event.set()
+
+ if self._state.buffer:
+ return self._state.buffer.popleft()
+ elif not self._state.open_send_channels:
+ raise EndOfStream
+
+ raise WouldBlock
+
+ async def receive(self) -> T_co:
+ await checkpoint()
+ try:
+ return self.receive_nowait()
+ except WouldBlock:
+ # Add ourselves in the queue
+ receive_event = Event()
+ receiver = _MemoryObjectItemReceiver[T_co]()
+ self._state.waiting_receivers[receive_event] = receiver
+
+ try:
+ await receive_event.wait()
+ finally:
+ self._state.waiting_receivers.pop(receive_event, None)
+
+ try:
+ return receiver.item
+ except AttributeError:
+ raise EndOfStream from None
+
+ def clone(self) -> MemoryObjectReceiveStream[T_co]:
+ """
+ Create a clone of this receive stream.
+
+ Each clone can be closed separately. Only when all clones have been closed will
+ the receiving end of the memory stream be considered closed by the sending ends.
+
+ :return: the cloned stream
+
+ """
+ if self._closed:
+ raise ClosedResourceError
+
+ return MemoryObjectReceiveStream(_state=self._state)
+
+ def close(self) -> None:
+ """
+ Close the stream.
+
+ This works the exact same way as :meth:`aclose`, but is provided as a special
+ case for the benefit of synchronous callbacks.
+
+ """
+ if not self._closed:
+ self._closed = True
+ self._state.open_receive_channels -= 1
+ if self._state.open_receive_channels == 0:
+ send_events = list(self._state.waiting_senders.keys())
+ for event in send_events:
+ event.set()
+
+ async def aclose(self) -> None:
+ self.close()
+
+ def statistics(self) -> MemoryObjectStreamStatistics:
+ """
+ Return statistics about the current state of this stream.
+
+ .. versionadded:: 3.0
+ """
+ return self._state.statistics()
+
+ def __enter__(self) -> MemoryObjectReceiveStream[T_co]:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.close()
+
+ def __del__(self) -> None:
+ if not self._closed:
+ warnings.warn(
+ f"Unclosed <{self.__class__.__name__} at {id(self):x}>",
+ ResourceWarning,
+ stacklevel=1,
+ source=self,
+ )
+
+
+@dataclass(eq=False)
+class MemoryObjectSendStream(ObjectSendStream[T_contra], Generic[T_contra]):
+ _state: _MemoryObjectStreamState[T_contra]
+ _closed: bool = field(init=False, default=False)
+
+ def __post_init__(self) -> None:
+ self._state.open_send_channels += 1
+
+ def send_nowait(self, item: T_contra) -> None:
+ """
+ Send an item immediately if it can be done without waiting.
+
+ :param item: the item to send
+ :raises ~anyio.ClosedResourceError: if this send stream has been closed
+ :raises ~anyio.BrokenResourceError: if the stream has been closed from the
+ receiving end
+ :raises ~anyio.WouldBlock: if the buffer is full and there are no tasks waiting
+ to receive
+
+ """
+ if self._closed:
+ raise ClosedResourceError
+ if not self._state.open_receive_channels:
+ raise BrokenResourceError
+
+ while self._state.waiting_receivers:
+ receive_event, receiver = self._state.waiting_receivers.popitem(last=False)
+ if not receiver.task_info.has_pending_cancellation():
+ receiver.item = item
+ receive_event.set()
+ return
+
+ if len(self._state.buffer) < self._state.max_buffer_size:
+ self._state.buffer.append(item)
+ else:
+ raise WouldBlock
+
+ async def send(self, item: T_contra) -> None:
+ """
+ Send an item to the stream.
+
+ If the buffer is full, this method blocks until there is again room in the
+ buffer or the item can be sent directly to a receiver.
+
+ :param item: the item to send
+ :raises ~anyio.ClosedResourceError: if this send stream has been closed
+ :raises ~anyio.BrokenResourceError: if the stream has been closed from the
+ receiving end
+
+ """
+ await checkpoint()
+ try:
+ self.send_nowait(item)
+ except WouldBlock:
+ # Wait until there's someone on the receiving end
+ send_event = Event()
+ self._state.waiting_senders[send_event] = item
+ try:
+ await send_event.wait()
+ except BaseException:
+ self._state.waiting_senders.pop(send_event, None)
+ raise
+
+ if send_event in self._state.waiting_senders:
+ del self._state.waiting_senders[send_event]
+ raise BrokenResourceError from None
+
+ def clone(self) -> MemoryObjectSendStream[T_contra]:
+ """
+ Create a clone of this send stream.
+
+ Each clone can be closed separately. Only when all clones have been closed will
+ the sending end of the memory stream be considered closed by the receiving ends.
+
+ :return: the cloned stream
+
+ """
+ if self._closed:
+ raise ClosedResourceError
+
+ return MemoryObjectSendStream(_state=self._state)
+
+ def close(self) -> None:
+ """
+ Close the stream.
+
+ This works the exact same way as :meth:`aclose`, but is provided as a special
+ case for the benefit of synchronous callbacks.
+
+ """
+ if not self._closed:
+ self._closed = True
+ self._state.open_send_channels -= 1
+ if self._state.open_send_channels == 0:
+ receive_events = list(self._state.waiting_receivers.keys())
+ self._state.waiting_receivers.clear()
+ for event in receive_events:
+ event.set()
+
+ async def aclose(self) -> None:
+ self.close()
+
+ def statistics(self) -> MemoryObjectStreamStatistics:
+ """
+ Return statistics about the current state of this stream.
+
+ .. versionadded:: 3.0
+ """
+ return self._state.statistics()
+
+ def __enter__(self) -> MemoryObjectSendStream[T_contra]:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.close()
+
+ def __del__(self) -> None:
+ if not self._closed:
+ warnings.warn(
+ f"Unclosed <{self.__class__.__name__} at {id(self):x}>",
+ ResourceWarning,
+ stacklevel=1,
+ source=self,
+ )
diff --git a/venv/lib/python3.11/site-packages/anyio/streams/stapled.py b/venv/lib/python3.11/site-packages/anyio/streams/stapled.py
new file mode 100644
index 000000000..9a7534563
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/streams/stapled.py
@@ -0,0 +1,160 @@
+from __future__ import annotations
+
+__all__ = (
+ "MultiListener",
+ "StapledByteStream",
+ "StapledObjectStream",
+)
+
+from collections.abc import Callable, Mapping, Sequence
+from dataclasses import dataclass
+from typing import Any, Generic, TypeVar
+
+from ..abc import (
+ ByteReceiveStream,
+ ByteSendStream,
+ ByteStream,
+ Listener,
+ ObjectReceiveStream,
+ ObjectSendStream,
+ ObjectStream,
+ TaskGroup,
+)
+
+T_Item = TypeVar("T_Item")
+T_Stream = TypeVar("T_Stream")
+
+
+@dataclass(eq=False)
+class StapledByteStream(ByteStream):
+ """
+ Combines two byte streams into a single, bidirectional byte stream.
+
+ Extra attributes will be provided from both streams, with the receive stream
+ providing the values in case of a conflict.
+
+ :param ByteSendStream send_stream: the sending byte stream
+ :param ByteReceiveStream receive_stream: the receiving byte stream
+ """
+
+ send_stream: ByteSendStream
+ receive_stream: ByteReceiveStream
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ return await self.receive_stream.receive(max_bytes)
+
+ async def send(self, item: bytes) -> None:
+ await self.send_stream.send(item)
+
+ async def send_eof(self) -> None:
+ await self.send_stream.aclose()
+
+ async def aclose(self) -> None:
+ await self.send_stream.aclose()
+ await self.receive_stream.aclose()
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return {
+ **self.send_stream.extra_attributes,
+ **self.receive_stream.extra_attributes,
+ }
+
+
+@dataclass(eq=False)
+class StapledObjectStream(ObjectStream[T_Item], Generic[T_Item]):
+ """
+ Combines two object streams into a single, bidirectional object stream.
+
+ Extra attributes will be provided from both streams, with the receive stream
+ providing the values in case of a conflict.
+
+ :param ObjectSendStream send_stream: the sending object stream
+ :param ObjectReceiveStream receive_stream: the receiving object stream
+ """
+
+ send_stream: ObjectSendStream[T_Item]
+ receive_stream: ObjectReceiveStream[T_Item]
+
+ async def receive(self) -> T_Item:
+ return await self.receive_stream.receive()
+
+ async def send(self, item: T_Item) -> None:
+ await self.send_stream.send(item)
+
+ def send_nowait(self, item: T_Item) -> None:
+ try:
+ send_nowait = self.send_stream.send_nowait # type: ignore[attr-defined]
+ except AttributeError as exc:
+ raise NotImplementedError(
+ f"'send_nowait' method not implemented in {type(self.send_stream)}"
+ ) from exc
+
+ send_nowait(item)
+
+ async def send_eof(self) -> None:
+ await self.send_stream.aclose()
+
+ async def aclose(self) -> None:
+ await self.send_stream.aclose()
+ await self.receive_stream.aclose()
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return {
+ **self.send_stream.extra_attributes,
+ **self.receive_stream.extra_attributes,
+ }
+
+
+@dataclass(eq=False)
+class MultiListener(Listener[T_Stream], Generic[T_Stream]):
+ """
+ Combines multiple listeners into one, serving connections from all of them at once.
+
+ Any MultiListeners in the given collection of listeners will have their listeners
+ moved into this one.
+
+ Extra attributes are provided from each listener, with each successive listener
+ overriding any conflicting attributes from the previous one.
+
+ :param listeners: listeners to serve
+ :type listeners: Sequence[Listener[T_Stream]]
+ """
+
+ listeners: Sequence[Listener[T_Stream]]
+
+ def __post_init__(self) -> None:
+ listeners: list[Listener[T_Stream]] = []
+ for listener in self.listeners:
+ if isinstance(listener, MultiListener):
+ listeners.extend(listener.listeners)
+ del listener.listeners[:] # type: ignore[attr-defined]
+ else:
+ listeners.append(listener)
+
+ self.listeners = listeners
+
+ async def serve(
+ self, handler: Callable[[T_Stream], Any], task_group: TaskGroup | None = None
+ ) -> None:
+ from .. import create_task_group
+
+ async with create_task_group() as tg:
+ for listener in self.listeners:
+ tg.start_soon(listener.serve, handler, task_group)
+
+ async def aclose(self) -> None:
+ for listener in self.listeners:
+ await listener.aclose()
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ attributes: dict = {}
+ for listener in self.listeners:
+ attributes.update(listener.extra_attributes)
+
+ return attributes
diff --git a/venv/lib/python3.11/site-packages/anyio/streams/text.py b/venv/lib/python3.11/site-packages/anyio/streams/text.py
new file mode 100644
index 000000000..296cd2504
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/streams/text.py
@@ -0,0 +1,176 @@
+from __future__ import annotations
+
+__all__ = (
+ "TextConnectable",
+ "TextReceiveStream",
+ "TextSendStream",
+ "TextStream",
+)
+
+import codecs
+import sys
+from collections.abc import Callable, Mapping
+from dataclasses import InitVar, dataclass, field
+from typing import Any
+
+from ..abc import (
+ AnyByteReceiveStream,
+ AnyByteSendStream,
+ AnyByteStream,
+ AnyByteStreamConnectable,
+ ObjectReceiveStream,
+ ObjectSendStream,
+ ObjectStream,
+ ObjectStreamConnectable,
+)
+
+if sys.version_info >= (3, 12):
+ from typing import override
+else:
+ from typing_extensions import override
+
+
+@dataclass(eq=False)
+class TextReceiveStream(ObjectReceiveStream[str]):
+ """
+ Stream wrapper that decodes bytes to strings using the given encoding.
+
+ Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any
+ completely received unicode characters as soon as they come in.
+
+ :param transport_stream: any bytes-based receive stream
+ :param encoding: character encoding to use for decoding bytes to strings (defaults
+ to ``utf-8``)
+ :param errors: handling scheme for decoding errors (defaults to ``strict``; see the
+ `codecs module documentation`_ for a comprehensive list of options)
+
+ .. _codecs module documentation:
+ https://docs.python.org/3/library/codecs.html#codec-objects
+ """
+
+ transport_stream: AnyByteReceiveStream
+ encoding: InitVar[str] = "utf-8"
+ errors: InitVar[str] = "strict"
+ _decoder: codecs.IncrementalDecoder = field(init=False)
+
+ def __post_init__(self, encoding: str, errors: str) -> None:
+ decoder_class = codecs.getincrementaldecoder(encoding)
+ self._decoder = decoder_class(errors=errors)
+
+ async def receive(self) -> str:
+ while True:
+ chunk = await self.transport_stream.receive()
+ decoded = self._decoder.decode(chunk)
+ if decoded:
+ return decoded
+
+ async def aclose(self) -> None:
+ await self.transport_stream.aclose()
+ self._decoder.reset()
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return self.transport_stream.extra_attributes
+
+
+@dataclass(eq=False)
+class TextSendStream(ObjectSendStream[str]):
+ """
+ Sends strings to the wrapped stream as bytes using the given encoding.
+
+ :param AnyByteSendStream transport_stream: any bytes-based send stream
+ :param str encoding: character encoding to use for encoding strings to bytes
+ (defaults to ``utf-8``)
+ :param str errors: handling scheme for encoding errors (defaults to ``strict``; see
+ the `codecs module documentation`_ for a comprehensive list of options)
+
+ .. _codecs module documentation:
+ https://docs.python.org/3/library/codecs.html#codec-objects
+ """
+
+ transport_stream: AnyByteSendStream
+ encoding: InitVar[str] = "utf-8"
+ errors: str = "strict"
+ _encoder: Callable[..., tuple[bytes, int]] = field(init=False)
+
+ def __post_init__(self, encoding: str) -> None:
+ self._encoder = codecs.getencoder(encoding)
+
+ async def send(self, item: str) -> None:
+ encoded = self._encoder(item, self.errors)[0]
+ await self.transport_stream.send(encoded)
+
+ async def aclose(self) -> None:
+ await self.transport_stream.aclose()
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return self.transport_stream.extra_attributes
+
+
+@dataclass(eq=False)
+class TextStream(ObjectStream[str]):
+ """
+ A bidirectional stream that decodes bytes to strings on receive and encodes strings
+ to bytes on send.
+
+ Extra attributes will be provided from both streams, with the receive stream
+ providing the values in case of a conflict.
+
+ :param AnyByteStream transport_stream: any bytes-based stream
+ :param str encoding: character encoding to use for encoding/decoding strings to/from
+ bytes (defaults to ``utf-8``)
+ :param str errors: handling scheme for encoding errors (defaults to ``strict``; see
+ the `codecs module documentation`_ for a comprehensive list of options)
+
+ .. _codecs module documentation:
+ https://docs.python.org/3/library/codecs.html#codec-objects
+ """
+
+ transport_stream: AnyByteStream
+ encoding: InitVar[str] = "utf-8"
+ errors: InitVar[str] = "strict"
+ _receive_stream: TextReceiveStream = field(init=False)
+ _send_stream: TextSendStream = field(init=False)
+
+ def __post_init__(self, encoding: str, errors: str) -> None:
+ self._receive_stream = TextReceiveStream(
+ self.transport_stream, encoding=encoding, errors=errors
+ )
+ self._send_stream = TextSendStream(
+ self.transport_stream, encoding=encoding, errors=errors
+ )
+
+ async def receive(self) -> str:
+ return await self._receive_stream.receive()
+
+ async def send(self, item: str) -> None:
+ await self._send_stream.send(item)
+
+ async def send_eof(self) -> None:
+ await self.transport_stream.send_eof()
+
+ async def aclose(self) -> None:
+ await self._send_stream.aclose()
+ await self._receive_stream.aclose()
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return {
+ **self._send_stream.extra_attributes,
+ **self._receive_stream.extra_attributes,
+ }
+
+
+class TextConnectable(ObjectStreamConnectable[str]):
+ def __init__(self, connectable: AnyByteStreamConnectable):
+ """
+ :param connectable: the bytestream endpoint to wrap
+
+ """
+ self.connectable = connectable
+
+ @override
+ async def connect(self) -> TextStream:
+ stream = await self.connectable.connect()
+ return TextStream(stream)
diff --git a/venv/lib/python3.11/site-packages/anyio/streams/tls.py b/venv/lib/python3.11/site-packages/anyio/streams/tls.py
new file mode 100644
index 000000000..b9ddf1d33
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/streams/tls.py
@@ -0,0 +1,436 @@
+from __future__ import annotations
+
+__all__ = (
+ "TLSAttribute",
+ "TLSConnectable",
+ "TLSListener",
+ "TLSStream",
+)
+
+import logging
+import re
+import ssl
+import sys
+from collections.abc import Callable, Mapping
+from dataclasses import dataclass
+from functools import wraps
+from ssl import SSLContext
+from typing import Any, TypeAlias, TypeVar
+
+from .. import (
+ BrokenResourceError,
+ EndOfStream,
+ aclose_forcefully,
+ get_cancelled_exc_class,
+ to_thread,
+)
+from .._core._typedattr import TypedAttributeSet, typed_attribute
+from ..abc import (
+ AnyByteStream,
+ AnyByteStreamConnectable,
+ ByteStream,
+ ByteStreamConnectable,
+ Listener,
+ TaskGroup,
+)
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+if sys.version_info >= (3, 12):
+ from typing import override
+else:
+ from typing_extensions import override
+
+T_Retval = TypeVar("T_Retval")
+PosArgsT = TypeVarTuple("PosArgsT")
+_PCTRTT: TypeAlias = tuple[tuple[str, str], ...]
+_PCTRTTT: TypeAlias = tuple[_PCTRTT, ...]
+
+
+class TLSAttribute(TypedAttributeSet):
+ """Contains Transport Layer Security related attributes."""
+
+ #: the selected ALPN protocol
+ alpn_protocol: str | None = typed_attribute()
+ #: the channel binding for type ``tls-unique``
+ channel_binding_tls_unique: bytes = typed_attribute()
+ #: the selected cipher
+ cipher: tuple[str, str, int] = typed_attribute()
+ #: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert`
+ # for more information)
+ peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute()
+ #: the peer certificate in binary form
+ peer_certificate_binary: bytes | None = typed_attribute()
+ #: ``True`` if this is the server side of the connection
+ server_side: bool = typed_attribute()
+ #: ciphers shared by the client during the TLS handshake (``None`` if this is the
+ #: client side)
+ shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute()
+ #: the :class:`~ssl.SSLObject` used for encryption
+ ssl_object: ssl.SSLObject = typed_attribute()
+ #: ``True`` if this stream does (and expects) a closing TLS handshake when the
+ #: stream is being closed
+ standard_compatible: bool = typed_attribute()
+ #: the TLS protocol version (e.g. ``TLSv1.2``)
+ tls_version: str = typed_attribute()
+
+
+@dataclass(eq=False)
+class TLSStream(ByteStream):
+ """
+ A stream wrapper that encrypts all sent data and decrypts received data.
+
+ This class has no public initializer; use :meth:`wrap` instead.
+ All extra attributes from :class:`~TLSAttribute` are supported.
+
+ :var AnyByteStream transport_stream: the wrapped stream
+
+ """
+
+ transport_stream: AnyByteStream
+ standard_compatible: bool
+ _ssl_object: ssl.SSLObject
+ _read_bio: ssl.MemoryBIO
+ _write_bio: ssl.MemoryBIO
+
+ @classmethod
+ async def wrap(
+ cls,
+ transport_stream: AnyByteStream,
+ *,
+ server_side: bool | None = None,
+ hostname: str | None = None,
+ ssl_context: ssl.SSLContext | None = None,
+ standard_compatible: bool = True,
+ ) -> TLSStream:
+ """
+ Wrap an existing stream with Transport Layer Security.
+
+ This performs a TLS handshake with the peer.
+
+ :param transport_stream: a bytes-transporting stream to wrap
+ :param server_side: ``True`` if this is the server side of the connection,
+ ``False`` if this is the client side (if omitted, will be set to ``False``
+ if ``hostname`` has been provided, ``False`` otherwise). Used only to create
+ a default context when an explicit context has not been provided.
+ :param hostname: host name of the peer (if host name checking is desired)
+ :param ssl_context: the SSLContext object to use (if not provided, a secure
+ default will be created)
+ :param standard_compatible: if ``False``, skip the closing handshake when
+ closing the connection, and don't raise an exception if the peer does the
+ same
+ :raises ~ssl.SSLError: if the TLS handshake fails
+
+ """
+ if server_side is None:
+ server_side = not hostname
+
+ if not ssl_context:
+ purpose = (
+ ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH
+ )
+ ssl_context = ssl.create_default_context(purpose)
+
+ # Re-enable detection of unexpected EOFs if it was disabled by Python
+ if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"):
+ ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF
+
+ bio_in = ssl.MemoryBIO()
+ bio_out = ssl.MemoryBIO()
+
+ # Resolve international host names using IDNA 2008.
+ # Otherwise wrap_bio() would resolve them with IDNA 2003.
+ if hostname is not None:
+ from .._core._sockets import idna2008_resolve
+
+ server_hostname: bytes | None = idna2008_resolve(hostname)
+ else:
+ server_hostname = None
+
+ # External SSLContext implementations may do blocking I/O in wrap_bio(),
+ # but the standard library implementation won't
+ if type(ssl_context) is ssl.SSLContext:
+ ssl_object = ssl_context.wrap_bio(
+ bio_in,
+ bio_out,
+ server_side=server_side,
+ server_hostname=server_hostname,
+ )
+ else:
+ ssl_object = await to_thread.run_sync(
+ ssl_context.wrap_bio,
+ bio_in,
+ bio_out,
+ server_side,
+ server_hostname,
+ None,
+ )
+
+ wrapper = cls(
+ transport_stream=transport_stream,
+ standard_compatible=standard_compatible,
+ _ssl_object=ssl_object,
+ _read_bio=bio_in,
+ _write_bio=bio_out,
+ )
+ await wrapper._call_sslobject_method(ssl_object.do_handshake)
+ return wrapper
+
+ async def _call_sslobject_method(
+ self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT]
+ ) -> T_Retval:
+ while True:
+ try:
+ result = func(*args)
+ except ssl.SSLWantReadError:
+ try:
+ # Flush any pending writes first
+ if self._write_bio.pending:
+ await self.transport_stream.send(self._write_bio.read())
+
+ data = await self.transport_stream.receive()
+ except EndOfStream:
+ self._read_bio.write_eof()
+ except OSError as exc:
+ self._read_bio.write_eof()
+ self._write_bio.write_eof()
+ raise BrokenResourceError from exc
+ else:
+ self._read_bio.write(data)
+ except ssl.SSLWantWriteError:
+ await self.transport_stream.send(self._write_bio.read())
+ except ssl.SSLSyscallError as exc:
+ self._read_bio.write_eof()
+ self._write_bio.write_eof()
+ raise BrokenResourceError from exc
+ except ssl.SSLError as exc:
+ self._read_bio.write_eof()
+ self._write_bio.write_eof()
+ if isinstance(exc, ssl.SSLEOFError) or (
+ exc.strerror and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror
+ ):
+ if self.standard_compatible:
+ raise BrokenResourceError from exc
+ else:
+ raise EndOfStream from None
+
+ raise
+ else:
+ # Flush any pending writes first
+ if self._write_bio.pending:
+ await self.transport_stream.send(self._write_bio.read())
+
+ return result
+
+ async def unwrap(self) -> tuple[AnyByteStream, bytes]:
+ """
+ Does the TLS closing handshake.
+
+ :return: a tuple of (wrapped byte stream, bytes left in the read buffer)
+
+ """
+ await self._call_sslobject_method(self._ssl_object.unwrap)
+ self._read_bio.write_eof()
+ self._write_bio.write_eof()
+ return self.transport_stream, self._read_bio.read()
+
+ async def aclose(self) -> None:
+ if self.standard_compatible:
+ try:
+ await self.unwrap()
+ except BaseException:
+ await aclose_forcefully(self.transport_stream)
+ raise
+
+ await self.transport_stream.aclose()
+
+ async def receive(self, max_bytes: int = 65536) -> bytes:
+ if max_bytes < 1:
+ raise ValueError("max_bytes must be a positive integer")
+
+ data = await self._call_sslobject_method(self._ssl_object.read, max_bytes)
+ if not data:
+ raise EndOfStream
+
+ return data
+
+ async def send(self, item: bytes) -> None:
+ await self._call_sslobject_method(self._ssl_object.write, item)
+
+ async def send_eof(self) -> None:
+ tls_version = self.extra(TLSAttribute.tls_version)
+ match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version)
+ if match:
+ major, minor = int(match.group(1)), int(match.group(2) or 0)
+ if (major, minor) < (1, 3):
+ raise NotImplementedError(
+ f"send_eof() requires at least TLSv1.3; current "
+ f"session uses {tls_version}"
+ )
+
+ raise NotImplementedError(
+ "send_eof() has not yet been implemented for TLS streams"
+ )
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return {
+ **self.transport_stream.extra_attributes,
+ TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol,
+ TLSAttribute.channel_binding_tls_unique: (
+ self._ssl_object.get_channel_binding
+ ),
+ TLSAttribute.cipher: self._ssl_object.cipher,
+ TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False),
+ TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert(
+ True
+ ),
+ TLSAttribute.server_side: lambda: self._ssl_object.server_side,
+ TLSAttribute.shared_ciphers: lambda: (
+ self._ssl_object.shared_ciphers()
+ if self._ssl_object.server_side
+ else None
+ ),
+ TLSAttribute.standard_compatible: lambda: self.standard_compatible,
+ TLSAttribute.ssl_object: lambda: self._ssl_object,
+ TLSAttribute.tls_version: self._ssl_object.version,
+ }
+
+
+@dataclass(eq=False)
+class TLSListener(Listener[TLSStream]):
+ """
+ A convenience listener that wraps another listener and auto-negotiates a TLS session
+ on every accepted connection.
+
+ If the TLS handshake times out or raises an exception,
+ :meth:`handle_handshake_error` is called to do whatever post-mortem processing is
+ deemed necessary.
+
+ Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute.
+
+ :param Listener listener: the listener to wrap
+ :param ssl_context: the SSL context object
+ :param standard_compatible: a flag passed through to :meth:`TLSStream.wrap`
+ :param handshake_timeout: time limit for the TLS handshake
+ (passed to :func:`~anyio.fail_after`)
+ """
+
+ listener: Listener[Any]
+ ssl_context: ssl.SSLContext
+ standard_compatible: bool = True
+ handshake_timeout: float = 30
+
+ @staticmethod
+ async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None:
+ """
+ Handle an exception raised during the TLS handshake.
+
+ This method does 3 things:
+
+ #. Forcefully closes the original stream
+ #. Logs the exception (unless it was a cancellation exception) using the
+ ``anyio.streams.tls`` logger
+ #. Reraises the exception if it was a base exception or a cancellation exception
+
+ :param exc: the exception
+ :param stream: the original stream
+
+ """
+ await aclose_forcefully(stream)
+
+ # Log all except cancellation exceptions
+ if not isinstance(exc, get_cancelled_exc_class()):
+ # CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using
+ # any asyncio implementation, so we explicitly pass the exception to log
+ # (https://github.com/python/cpython/issues/108668). Trio does not have this
+ # issue because it works around the CPython bug.
+ logging.getLogger(__name__).exception(
+ "Error during TLS handshake", exc_info=exc
+ )
+
+ # Only reraise base exceptions and cancellation exceptions
+ if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()):
+ raise # noqa: PLE0704
+
+ async def serve(
+ self,
+ handler: Callable[[TLSStream], Any],
+ task_group: TaskGroup | None = None,
+ ) -> None:
+ @wraps(handler)
+ async def handler_wrapper(stream: AnyByteStream) -> None:
+ from .. import fail_after
+
+ try:
+ with fail_after(self.handshake_timeout):
+ wrapped_stream = await TLSStream.wrap(
+ stream,
+ ssl_context=self.ssl_context,
+ standard_compatible=self.standard_compatible,
+ )
+ except BaseException as exc:
+ await self.handle_handshake_error(exc, stream)
+ else:
+ await handler(wrapped_stream)
+
+ await self.listener.serve(handler_wrapper, task_group)
+
+ async def aclose(self) -> None:
+ await self.listener.aclose()
+
+ @property
+ def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
+ return {
+ TLSAttribute.standard_compatible: lambda: self.standard_compatible,
+ }
+
+
+class TLSConnectable(ByteStreamConnectable):
+ """
+ Wraps another connectable and does TLS negotiation after a successful connection.
+
+ :param connectable: the connectable to wrap
+ :param hostname: host name of the server (if host name checking is desired)
+ :param ssl_context: the SSLContext object to use (if not provided, a secure default
+ will be created)
+ :param standard_compatible: if ``False``, skip the closing handshake when closing
+ the connection, and don't raise an exception if the server does the same
+ """
+
+ def __init__(
+ self,
+ connectable: AnyByteStreamConnectable,
+ *,
+ hostname: str | None = None,
+ ssl_context: ssl.SSLContext | None = None,
+ standard_compatible: bool = True,
+ ) -> None:
+ self.connectable = connectable
+ self.ssl_context: SSLContext = ssl_context or ssl.create_default_context(
+ ssl.Purpose.SERVER_AUTH
+ )
+ if not isinstance(self.ssl_context, ssl.SSLContext):
+ raise TypeError(
+ "ssl_context must be an instance of ssl.SSLContext, not "
+ f"{type(self.ssl_context).__name__}"
+ )
+ self.hostname = hostname
+ self.standard_compatible = standard_compatible
+
+ @override
+ async def connect(self) -> TLSStream:
+ stream = await self.connectable.connect()
+ try:
+ return await TLSStream.wrap(
+ stream,
+ hostname=self.hostname,
+ ssl_context=self.ssl_context,
+ standard_compatible=self.standard_compatible,
+ )
+ except BaseException:
+ await aclose_forcefully(stream)
+ raise
diff --git a/venv/lib/python3.11/site-packages/anyio/to_interpreter.py b/venv/lib/python3.11/site-packages/anyio/to_interpreter.py
new file mode 100644
index 000000000..54ef4a8b8
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/to_interpreter.py
@@ -0,0 +1,246 @@
+from __future__ import annotations
+
+__all__ = (
+ "current_default_interpreter_limiter",
+ "run_sync",
+)
+
+import atexit
+import os
+import sys
+from collections import deque
+from collections.abc import Callable
+from typing import Any, Final, TypeVar
+
+from . import current_time, to_thread
+from ._core._exceptions import BrokenWorkerInterpreter
+from ._core._synchronization import CapacityLimiter
+from .lowlevel import RunVar
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+if sys.version_info >= (3, 14):
+ from concurrent.interpreters import ExecutionFailed, create
+
+ def _interp_call(
+ func: Callable[..., Any], args: tuple[Any, ...]
+ ) -> tuple[Any, bool]:
+ try:
+ retval = func(*args)
+ except BaseException as exc:
+ return exc, True
+ else:
+ return retval, False
+
+ class _Worker:
+ last_used: float = 0
+
+ def __init__(self) -> None:
+ self._interpreter = create()
+
+ def destroy(self) -> None:
+ self._interpreter.close()
+
+ def call(
+ self,
+ func: Callable[..., T_Retval],
+ args: tuple[Any, ...],
+ ) -> T_Retval:
+ try:
+ res, is_exception = self._interpreter.call(_interp_call, func, args)
+ except ExecutionFailed as exc:
+ raise BrokenWorkerInterpreter(exc.excinfo) from exc
+
+ if is_exception:
+ raise res
+
+ return res
+elif sys.version_info >= (3, 13):
+ import _interpqueues
+ import _interpreters
+
+ UNBOUND: Final = 2 # I have no clue how this works, but it was used in the stdlib
+ FMT_UNPICKLED: Final = 0
+ FMT_PICKLED: Final = 1
+ QUEUE_PICKLE_ARGS: Final = (FMT_PICKLED, UNBOUND)
+ QUEUE_UNPICKLE_ARGS: Final = (FMT_UNPICKLED, UNBOUND)
+
+ _run_func = compile(
+ """
+import _interpqueues
+from _interpreters import NotShareableError
+from pickle import loads, dumps, HIGHEST_PROTOCOL
+
+QUEUE_PICKLE_ARGS = (1, 2)
+QUEUE_UNPICKLE_ARGS = (0, 2)
+
+item = _interpqueues.get(queue_id)[0]
+try:
+ func, args = loads(item)
+ retval = func(*args)
+except BaseException as exc:
+ is_exception = True
+ retval = exc
+else:
+ is_exception = False
+
+try:
+ _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_UNPICKLE_ARGS)
+except NotShareableError:
+ retval = dumps(retval, HIGHEST_PROTOCOL)
+ _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_PICKLE_ARGS)
+ """,
+ "",
+ "exec",
+ )
+
+ class _Worker:
+ last_used: float = 0
+
+ def __init__(self) -> None:
+ self._interpreter_id = _interpreters.create()
+ self._queue_id = _interpqueues.create(1, *QUEUE_UNPICKLE_ARGS)
+ _interpreters.set___main___attrs(
+ self._interpreter_id, {"queue_id": self._queue_id}
+ )
+
+ def destroy(self) -> None:
+ _interpqueues.destroy(self._queue_id)
+ _interpreters.destroy(self._interpreter_id)
+
+ def call(
+ self,
+ func: Callable[..., T_Retval],
+ args: tuple[Any, ...],
+ ) -> T_Retval:
+ import pickle
+
+ item = pickle.dumps((func, args), pickle.HIGHEST_PROTOCOL)
+ _interpqueues.put(self._queue_id, item, *QUEUE_PICKLE_ARGS)
+ exc_info = _interpreters.exec(self._interpreter_id, _run_func)
+ if exc_info:
+ raise BrokenWorkerInterpreter(exc_info)
+
+ res = _interpqueues.get(self._queue_id)
+ (res, is_exception), fmt = res[:2]
+ if fmt == FMT_PICKLED:
+ res = pickle.loads(res)
+
+ if is_exception:
+ raise res
+
+ return res
+else:
+
+ class _Worker:
+ last_used: float = 0
+
+ def __init__(self) -> None:
+ raise RuntimeError("subinterpreters require at least Python 3.13")
+
+ def call(
+ self,
+ func: Callable[..., T_Retval],
+ args: tuple[Any, ...],
+ ) -> T_Retval:
+ raise NotImplementedError
+
+ def destroy(self) -> None:
+ pass
+
+
+DEFAULT_CPU_COUNT: Final = 8 # this is just an arbitrarily selected value
+MAX_WORKER_IDLE_TIME = (
+ 30 # seconds a subinterpreter can be idle before becoming eligible for pruning
+)
+
+T_Retval = TypeVar("T_Retval")
+PosArgsT = TypeVarTuple("PosArgsT")
+
+_idle_workers = RunVar[deque[_Worker]]("_available_workers")
+_default_interpreter_limiter = RunVar[CapacityLimiter]("_default_interpreter_limiter")
+
+
+def _stop_workers(workers: deque[_Worker]) -> None:
+ for worker in workers:
+ worker.destroy()
+
+ workers.clear()
+
+
+async def run_sync(
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ *args: Unpack[PosArgsT],
+ limiter: CapacityLimiter | None = None,
+) -> T_Retval:
+ """
+ Call the given function with the given arguments in a subinterpreter.
+
+ .. warning:: On Python 3.13, the :mod:`concurrent.interpreters` module was not yet
+ available, so the code path for that Python version relies on an undocumented,
+ private API. As such, it is recommended to not rely on this function for anything
+ mission-critical on Python 3.13.
+
+ :param func: a callable
+ :param args: the positional arguments for the callable
+ :param limiter: capacity limiter to use to limit the total number of subinterpreters
+ running (if omitted, the default limiter is used)
+ :return: the result of the call
+ :raises BrokenWorkerInterpreter: if there's an internal error in a subinterpreter
+
+ """
+ if limiter is None:
+ limiter = current_default_interpreter_limiter()
+
+ try:
+ idle_workers = _idle_workers.get()
+ except LookupError:
+ idle_workers = deque()
+ _idle_workers.set(idle_workers)
+ atexit.register(_stop_workers, idle_workers)
+
+ async with limiter:
+ try:
+ worker = idle_workers.pop()
+ except IndexError:
+ worker = _Worker()
+
+ try:
+ return await to_thread.run_sync(
+ worker.call,
+ func,
+ args,
+ limiter=limiter,
+ )
+ finally:
+ # Prune workers that have been idle for too long
+ now = current_time()
+ while idle_workers:
+ if now - idle_workers[0].last_used <= MAX_WORKER_IDLE_TIME:
+ break
+
+ await to_thread.run_sync(idle_workers.popleft().destroy, limiter=limiter)
+
+ worker.last_used = current_time()
+ idle_workers.append(worker)
+
+
+def current_default_interpreter_limiter() -> CapacityLimiter:
+ """
+ Return the capacity limiter used by default to limit the number of concurrently
+ running subinterpreters.
+
+ Defaults to the number of CPU cores.
+
+ :return: a capacity limiter object
+
+ """
+ try:
+ return _default_interpreter_limiter.get()
+ except LookupError:
+ limiter = CapacityLimiter(os.cpu_count() or DEFAULT_CPU_COUNT)
+ _default_interpreter_limiter.set(limiter)
+ return limiter
diff --git a/venv/lib/python3.11/site-packages/anyio/to_process.py b/venv/lib/python3.11/site-packages/anyio/to_process.py
new file mode 100644
index 000000000..584128ec3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/to_process.py
@@ -0,0 +1,269 @@
+from __future__ import annotations
+
+__all__ = (
+ "current_default_process_limiter",
+ "process_worker",
+ "run_sync",
+)
+
+import os
+import pickle
+import runpy
+import subprocess
+import sys
+from collections import deque
+from collections.abc import Callable
+from types import ModuleType
+from typing import TypeVar, cast
+
+from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class
+from ._core._exceptions import BrokenWorkerProcess
+from ._core._subprocesses import open_process
+from ._core._synchronization import CapacityLimiter
+from ._core._tasks import CancelScope, fail_after
+from .abc import ByteReceiveStream, ByteSendStream, Process
+from .lowlevel import RunVar, checkpoint_if_cancelled
+from .streams.buffered import BufferedByteReceiveStream
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+WORKER_MAX_IDLE_TIME = 300 # 5 minutes
+
+T_Retval = TypeVar("T_Retval")
+PosArgsT = TypeVarTuple("PosArgsT")
+
+_process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers")
+_process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar(
+ "_process_pool_idle_workers"
+)
+_default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter")
+
+
+async def run_sync( # type: ignore[return]
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ *args: Unpack[PosArgsT],
+ cancellable: bool = False,
+ limiter: CapacityLimiter | None = None,
+) -> T_Retval:
+ """
+ Call the given function with the given arguments in a worker process.
+
+ If the ``cancellable`` option is enabled and the task waiting for its completion is
+ cancelled, the worker process running it will be abruptly terminated using SIGKILL
+ (or ``terminateProcess()`` on Windows).
+
+ :param func: a callable
+ :param args: positional arguments for the callable
+ :param cancellable: ``True`` to allow cancellation of the operation while it's
+ running
+ :param limiter: capacity limiter to use to limit the total amount of processes
+ running (if omitted, the default limiter is used)
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+ :return: an awaitable that yields the return value of the function.
+
+ """
+
+ async def send_raw_command(pickled_cmd: bytes) -> object:
+ try:
+ await stdin.send(pickled_cmd)
+ response = await buffered.receive_until(b"\n", 50)
+ status, length = response.split(b" ")
+ if status not in (b"RETURN", b"EXCEPTION"):
+ raise RuntimeError(
+ f"Worker process returned unexpected response: {response!r}"
+ )
+
+ pickled_response = await buffered.receive_exactly(int(length))
+ except BaseException as exc:
+ workers.discard(process)
+ try:
+ process.kill()
+ with CancelScope(shield=True):
+ await process.aclose()
+ except ProcessLookupError:
+ pass
+
+ if isinstance(exc, get_cancelled_exc_class()):
+ raise
+ else:
+ raise BrokenWorkerProcess from exc
+
+ retval = pickle.loads(pickled_response)
+ if status == b"EXCEPTION":
+ assert isinstance(retval, BaseException)
+ raise retval
+ else:
+ return retval
+
+ # First pickle the request before trying to reserve a worker process
+ await checkpoint_if_cancelled()
+ request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL)
+
+ # If this is the first run in this event loop thread, set up the necessary variables
+ try:
+ workers = _process_pool_workers.get()
+ idle_workers = _process_pool_idle_workers.get()
+ except LookupError:
+ workers = set()
+ idle_workers = deque()
+ _process_pool_workers.set(workers)
+ _process_pool_idle_workers.set(idle_workers)
+ get_async_backend().setup_process_pool_exit_at_shutdown(workers)
+
+ async with limiter or current_default_process_limiter():
+ # Pop processes from the pool (starting from the most recently used) until we
+ # find one that hasn't exited yet
+ process: Process
+ while idle_workers:
+ process, _idle_since = idle_workers.pop()
+ if process.returncode is None:
+ stdin = cast(ByteSendStream, process.stdin)
+ buffered = BufferedByteReceiveStream(
+ cast(ByteReceiveStream, process.stdout)
+ )
+
+ # Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME
+ # seconds or longer
+ now = current_time()
+ killed_processes: list[Process] = []
+ while idle_workers:
+ if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME:
+ break
+
+ process_to_kill, _idle_since = idle_workers.popleft()
+ process_to_kill.kill()
+ workers.remove(process_to_kill)
+ killed_processes.append(process_to_kill)
+
+ with CancelScope(shield=True):
+ for killed_process in killed_processes:
+ await killed_process.aclose()
+
+ break
+
+ workers.remove(process)
+ else:
+ command = [sys.executable, "-u", "-m", __name__]
+ process = await open_process(
+ command, stdin=subprocess.PIPE, stdout=subprocess.PIPE
+ )
+ try:
+ stdin = cast(ByteSendStream, process.stdin)
+ buffered = BufferedByteReceiveStream(
+ cast(ByteReceiveStream, process.stdout)
+ )
+ with fail_after(20):
+ message = await buffered.receive(6)
+
+ if message != b"READY\n":
+ raise BrokenWorkerProcess(
+ f"Worker process returned unexpected response: {message!r}"
+ )
+
+ main_module_path = getattr(sys.modules["__main__"], "__file__", None)
+ pickled = pickle.dumps(
+ ("init", sys.path, main_module_path),
+ protocol=pickle.HIGHEST_PROTOCOL,
+ )
+ await send_raw_command(pickled)
+ except (BrokenWorkerProcess, get_cancelled_exc_class()):
+ raise
+ except BaseException as exc:
+ process.kill()
+ raise BrokenWorkerProcess(
+ "Error during worker process initialization"
+ ) from exc
+
+ workers.add(process)
+
+ with CancelScope(shield=not cancellable):
+ try:
+ return cast(T_Retval, await send_raw_command(request))
+ finally:
+ if process in workers:
+ idle_workers.append((process, current_time()))
+
+
+def current_default_process_limiter() -> CapacityLimiter:
+ """
+ Return the capacity limiter that is used by default to limit the number of worker
+ processes.
+
+ :return: a capacity limiter object
+
+ """
+ try:
+ return _default_process_limiter.get()
+ except LookupError:
+ limiter = CapacityLimiter(os.cpu_count() or 2)
+ _default_process_limiter.set(limiter)
+ return limiter
+
+
+def process_worker() -> None:
+ # Redirect standard streams to os.devnull so that user code won't interfere with the
+ # parent-worker communication
+ stdin = sys.stdin
+ stdout = sys.stdout
+ sys.stdin = open(os.devnull)
+ sys.stdout = open(os.devnull, "w")
+ sys.stderr = open(os.devnull, "w")
+
+ stdout.buffer.write(b"READY\n")
+ while True:
+ retval = exception = None
+ try:
+ command, *args = pickle.load(stdin.buffer)
+ except EOFError:
+ return
+ except BaseException as exc:
+ exception = exc
+ else:
+ if command == "run":
+ func, args = args
+ try:
+ retval = func(*args)
+ except BaseException as exc:
+ exception = exc
+ elif command == "init":
+ main_module_path: str | None
+ sys.path, main_module_path = args
+ del sys.modules["__main__"]
+ if main_module_path and os.path.isfile(main_module_path):
+ # Load the parent's main module but as __mp_main__ instead of
+ # __main__ (like multiprocessing does) to avoid infinite recursion
+ try:
+ main = ModuleType("__mp_main__")
+ main_content = runpy.run_path(
+ main_module_path, run_name="__mp_main__"
+ )
+ main.__dict__.update(main_content)
+ sys.modules["__main__"] = sys.modules["__mp_main__"] = main
+ except BaseException as exc:
+ exception = exc
+ try:
+ if exception is not None:
+ status = b"EXCEPTION"
+ pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL)
+ else:
+ status = b"RETURN"
+ pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL)
+ except BaseException as exc:
+ exception = exc
+ status = b"EXCEPTION"
+ pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL)
+
+ stdout.buffer.write(b"%s %d\n" % (status, len(pickled)))
+ stdout.buffer.write(pickled)
+
+ # Respect SIGTERM
+ if isinstance(exception, SystemExit):
+ raise exception
+
+
+if __name__ == "__main__":
+ process_worker()
diff --git a/venv/lib/python3.11/site-packages/anyio/to_thread.py b/venv/lib/python3.11/site-packages/anyio/to_thread.py
new file mode 100644
index 000000000..5cdb7253e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/anyio/to_thread.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+__all__ = (
+ "current_default_thread_limiter",
+ "run_sync",
+)
+
+import sys
+from collections.abc import Callable
+from typing import TYPE_CHECKING, TypeVar
+from warnings import warn
+
+from ._core._eventloop import get_async_backend
+
+if TYPE_CHECKING:
+ from ._core._synchronization import CapacityLimiter
+
+if sys.version_info >= (3, 11):
+ from typing import TypeVarTuple, Unpack
+else:
+ from typing_extensions import TypeVarTuple, Unpack
+
+T_Retval = TypeVar("T_Retval")
+PosArgsT = TypeVarTuple("PosArgsT")
+
+
+async def run_sync(
+ func: Callable[[Unpack[PosArgsT]], T_Retval],
+ *args: Unpack[PosArgsT],
+ abandon_on_cancel: bool = False,
+ cancellable: bool | None = None,
+ limiter: CapacityLimiter | None = None,
+) -> T_Retval:
+ """
+ Call the given function with the given arguments in a worker thread.
+
+ If the ``abandon_on_cancel`` option is enabled and the task waiting for its
+ completion is cancelled, the thread will still run its course but its
+ return value (or any raised exception) will be ignored.
+
+ :param func: a callable
+ :param args: positional arguments for the callable
+ :param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run
+ unchecked on own) if the host task is cancelled, ``False`` to ignore
+ cancellations in the host task until the operation has completed in the worker
+ thread
+ :param cancellable: deprecated alias of ``abandon_on_cancel``; will override
+ ``abandon_on_cancel`` if both parameters are passed
+ :param limiter: capacity limiter to use to limit the total amount of threads running
+ (if omitted, the default limiter is used)
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+ :return: an awaitable that yields the return value of the function.
+
+ """
+ if cancellable is not None:
+ abandon_on_cancel = cancellable
+ warn(
+ "The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is "
+ "deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ return await get_async_backend().run_sync_in_worker_thread(
+ func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter
+ )
+
+
+def current_default_thread_limiter() -> CapacityLimiter:
+ """
+ Return the capacity limiter that is used by default to limit the number of
+ concurrent threads.
+
+ :return: a capacity limiter object
+ :raises NoEventLoopError: if no supported asynchronous event loop is running in the
+ current thread
+
+ """
+ return get_async_backend().current_default_thread_limiter()
diff --git a/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/INSTALLER
new file mode 100644
index 000000000..a1b589e38
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/METADATA b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/METADATA
new file mode 100644
index 000000000..a891c71ab
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/METADATA
@@ -0,0 +1,34 @@
+Metadata-Version: 2.4
+Name: ast_serialize
+Version: 0.12.1
+Classifier: Development Status :: 3 - Alpha
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
+Classifier: Programming Language :: Rust
+Classifier: Topic :: Software Development
+Classifier: Typing :: Typed
+License-File: LICENSE
+License-File: crates/LICENSE
+Summary: Python bindings for mypy AST serialization
+Author-email: Jukka Lehtosalo
+License-Expression: MIT
+Requires-Python: >=3.7
+Description-Content-Type: text/x-rst
+Project-URL: Homepage, https://github.com/mypyc/ast_serialize
+Project-URL: Issues, https://github.com/mypyc/ast_serialize/issues
+
+ast_serialize - Python Parser and AST Serializer
+================================================
+
+This is a fast Python extension for parsing Python files and serializing the AST using
+the native binary format used by mypy. This will eventually replace the current mypy parser,
+which uses the Python stdlib ``ast`` module for parsing.
+
diff --git a/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/RECORD b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/RECORD
new file mode 100644
index 000000000..2aed1e4f3
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/RECORD
@@ -0,0 +1,12 @@
+ast_serialize-0.12.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+ast_serialize-0.12.1.dist-info/METADATA,sha256=yNDJZ0kWUE2Rc0Dwuvy1sdJUmipT1IOfzQI0YwmjyJ0,1405
+ast_serialize-0.12.1.dist-info/RECORD,,
+ast_serialize-0.12.1.dist-info/WHEEL,sha256=XS1u14cCmBlzn-6xIL0ybY70b1bpNwxVCNWkI3Vd3Gw,143
+ast_serialize-0.12.1.dist-info/licenses/LICENSE,sha256=25C4_QrhdIEXq0Z2gKUZO_0FejEZhGy1PuO2TxZXezs,1281
+ast_serialize-0.12.1.dist-info/licenses/crates/LICENSE,sha256=JZfYVBIrd93HGXFWTKI1CjdghXXOMkrcVlCisgUcjxg,20731
+ast_serialize-0.12.1.dist-info/sboms/mypy_parser.cyclonedx.json,sha256=VXJTwt7-cfFjj7jyryqwYU40ZFHvCiemjfHhQEUoazM,116125
+ast_serialize/__init__.py,sha256=mXw0A2FKxBSTePe9oon42XOGM80x6EzbnNJWoTn_Cac,135
+ast_serialize/__init__.pyi,sha256=GNAFnhTFJYZ669JbDbOtgaqLqt2pAe-wp98XJIzSw5I,1044
+ast_serialize/__pycache__/__init__.cpython-311.pyc,,
+ast_serialize/ast_serialize.abi3.so,sha256=oiVrukfkMpLY8AEjrqUqNhCnv0SVup8Qt1PViOJuWJw,3061936
+ast_serialize/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
diff --git a/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/WHEEL b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/WHEEL
new file mode 100644
index 000000000..bb06b4e76
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: maturin (1.15.0)
+Root-Is-Purelib: false
+Tag: cp39-abi3-manylinux_2_17_x86_64
+Tag: cp39-abi3-manylinux2014_x86_64
diff --git a/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/licenses/LICENSE
new file mode 100644
index 000000000..c3b030487
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/licenses/LICENSE
@@ -0,0 +1,27 @@
+ast_serialize is licensed under the terms of the MIT license, reproduced below.
+Ruff crates are licensed under the terms of the MIT license, reproduced
+in crates/LICENSE.
+
+= = = = =
+
+The MIT License
+
+Copyright (c) 2025-2026 Jukka Lehtosalo and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/licenses/crates/LICENSE b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/licenses/crates/LICENSE
new file mode 100644
index 000000000..655a0c76f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/licenses/crates/LICENSE
@@ -0,0 +1,430 @@
+MIT License
+
+Copyright (c) 2022 Charles Marsh
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+end of terms and conditions
+
+The externally maintained libraries from which parts of the Software is derived
+are:
+
+- autoflake, licensed as follows:
+ """
+ Copyright (C) 2012-2018 Steven Myint
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
+ this software and associated documentation files (the "Software"), to deal in
+ the Software without restriction, including without limitation the rights to
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ of the Software, and to permit persons to whom the Software is furnished to do
+ so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- autotyping, licensed as follows:
+ """
+ MIT License
+
+ Copyright (c) 2023 Jelle Zijlstra
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- Flake8, licensed as follows:
+ """
+ == Flake8 License (MIT) ==
+
+ Copyright (C) 2011-2013 Tarek Ziade
+ Copyright (C) 2012-2016 Ian Cordasco
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
+ this software and associated documentation files (the "Software"), to deal in
+ the Software without restriction, including without limitation the rights to
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ of the Software, and to permit persons to whom the Software is furnished to do
+ so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- flake8-eradicate, licensed as follows:
+ """
+ MIT License
+
+ Copyright (c) 2018 Nikita Sobolev
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- flake8-pyi, licensed as follows:
+ """
+ The MIT License (MIT)
+
+ Copyright (c) 2016 Łukasz Langa
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- flake8-simplify, licensed as follows:
+ """
+ MIT License
+
+ Copyright (c) 2020 Martin Thoma
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- isort, licensed as follows:
+ """
+ The MIT License (MIT)
+
+ Copyright (c) 2013 Timothy Edmund Crosley
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ """
+
+- pygrep-hooks, licensed as follows:
+ """
+ Copyright (c) 2018 Anthony Sottile
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ """
+
+- pycodestyle, licensed as follows:
+ """
+ Copyright © 2006-2009 Johann C. Rocholl
+ Copyright © 2009-2014 Florent Xicluna
+ Copyright © 2014-2020 Ian Lee
+
+ Licensed under the terms of the Expat License
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation files
+ (the "Software"), to deal in the Software without restriction,
+ including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software,
+ and to permit persons to whom the Software is furnished to do so,
+ subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- pydocstyle, licensed as follows:
+ """
+ Copyright (c) 2012 GreenSteam,
+
+ Copyright (c) 2014-2020 Amir Rachum,
+
+ Copyright (c) 2020 Sambhav Kothari,
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
+ this software and associated documentation files (the "Software"), to deal in
+ the Software without restriction, including without limitation the rights to
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ of the Software, and to permit persons to whom the Software is furnished to do
+ so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- Pyflakes, licensed as follows:
+ """
+ Copyright 2005-2011 Divmod, Inc.
+ Copyright 2013-2014 Florent Xicluna
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ """
+
+- Pyright, licensed as follows:
+ """
+ MIT License
+
+ Pyright - A static type checker for the Python language
+ Copyright (c) Microsoft Corporation. All rights reserved.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE
+ """
+
+- pyupgrade, licensed as follows:
+ """
+ Copyright (c) 2017 Anthony Sottile
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ """
+
+- rome/tools, licensed under the MIT license:
+ """
+ MIT License
+
+ Copyright (c) Rome Tools, Inc. and its affiliates.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- RustPython, licensed as follows:
+ """
+ MIT License
+
+ Copyright (c) 2020 RustPython Team
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
+
+- rust-analyzer/text-size, licensed under the MIT license:
+ """
+ Permission is hereby granted, free of charge, to any
+ person obtaining a copy of this software and associated
+ documentation files (the "Software"), to deal in the
+ Software without restriction, including without
+ limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of
+ the Software, and to permit persons to whom the Software
+ is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice
+ shall be included in all copies or substantial portions
+ of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+ """
diff --git a/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/sboms/mypy_parser.cyclonedx.json b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/sboms/mypy_parser.cyclonedx.json
new file mode 100644
index 000000000..d1a211b20
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/ast_serialize-0.12.1.dist-info/sboms/mypy_parser.cyclonedx.json
@@ -0,0 +1,3621 @@
+{
+ "bomFormat": "CycloneDX",
+ "specVersion": "1.5",
+ "version": 1,
+ "serialNumber": "urn:uuid:6074e901-8b99-4ac3-a5c2-554722e5ea71",
+ "metadata": {
+ "timestamp": "2026-10-03T12:19:57.667286105Z",
+ "tools": [
+ {
+ "vendor": "CycloneDX",
+ "name": "cargo-cyclonedx",
+ "version": "0.5.9"
+ }
+ ],
+ "authors": [
+ {
+ "name": "Jukka Lehtosalo"
+ }
+ ],
+ "component": {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize#mypy_parser@0.12.1",
+ "author": "Jukka Lehtosalo",
+ "name": "mypy_parser",
+ "version": "0.12.1",
+ "description": "Python extension that serializes Ruff AST for mypy",
+ "scope": "required",
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/mypy_parser@0.12.1?download_url=file://.",
+ "components": [
+ {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize#mypy_parser@0.12.1 bin-target-0",
+ "name": "ast_serialize",
+ "version": "0.12.1",
+ "purl": "pkg:cargo/mypy_parser@0.12.1?download_url=file://.#src/lib.rs"
+ }
+ ]
+ },
+ "properties": [
+ {
+ "name": "cdx:rustc:sbom:target:all_targets",
+ "value": "true"
+ }
+ ]
+ },
+ "components": [
+ {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize/crates/ruff_cache#0.0.9-ast-serialize.0.12.1",
+ "author": "Charlie Marsh ",
+ "name": "ruff_cache",
+ "version": "0.0.9-ast-serialize.0.12.1",
+ "description": "This is an internal component crate of Ruff",
+ "scope": "required",
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/ruff_cache@0.0.9-ast-serialize.0.12.1?download_url=file://crates/ruff_cache",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "website",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/astral-sh/ruff"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize/crates/ruff_python_ast#0.0.9-ast-serialize.0.12.1",
+ "author": "Charlie Marsh ",
+ "name": "ruff_python_ast",
+ "version": "0.0.9-ast-serialize.0.12.1",
+ "description": "This is an internal component crate of Ruff",
+ "scope": "required",
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/ruff_python_ast@0.0.9-ast-serialize.0.12.1?download_url=file://crates/ruff_python_ast",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "website",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/astral-sh/ruff"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize/crates/ruff_python_parser#0.0.9-ast-serialize.0.12.1",
+ "author": "Charlie Marsh , RustPython Team",
+ "name": "ruff_python_parser",
+ "version": "0.0.9-ast-serialize.0.12.1",
+ "description": "This is an internal component crate of Ruff",
+ "scope": "required",
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/ruff_python_parser@0.0.9-ast-serialize.0.12.1?download_url=file://crates/ruff_python_parser",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "website",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/astral-sh/ruff"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize/crates/ruff_python_trivia#0.0.9-ast-serialize.0.12.1",
+ "author": "Charlie Marsh ",
+ "name": "ruff_python_trivia",
+ "version": "0.0.9-ast-serialize.0.12.1",
+ "description": "This is an internal component crate of Ruff",
+ "scope": "required",
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/ruff_python_trivia@0.0.9-ast-serialize.0.12.1?download_url=file://crates/ruff_python_trivia",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "website",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/astral-sh/ruff"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize/crates/ruff_source_file#0.0.9-ast-serialize.0.12.1",
+ "author": "Charlie Marsh ",
+ "name": "ruff_source_file",
+ "version": "0.0.9-ast-serialize.0.12.1",
+ "description": "This is an internal component crate of Ruff",
+ "scope": "required",
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/ruff_source_file@0.0.9-ast-serialize.0.12.1?download_url=file://crates/ruff_source_file",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "website",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/astral-sh/ruff"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "path+file:///home/runner/work/ast_serialize/ast_serialize/crates/ruff_text_size#0.0.9-ast-serialize.0.12.1",
+ "author": "Charlie Marsh ",
+ "name": "ruff_text_size",
+ "version": "0.0.9-ast-serialize.0.12.1",
+ "description": "This is an internal component crate of Ruff",
+ "scope": "required",
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/ruff_text_size@0.0.9-ast-serialize.0.12.1?download_url=file://crates/ruff_text_size",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "website",
+ "url": "https://docs.astral.sh/ruff"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/astral-sh/ruff"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.4",
+ "author": "Andrew Gallant ",
+ "name": "aho-corasick",
+ "version": "1.1.4",
+ "description": "Fast multiple substring searching.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "Unlicense OR MIT"
+ }
+ ],
+ "purl": "pkg:cargo/aho-corasick@1.1.4",
+ "externalReferences": [
+ {
+ "type": "website",
+ "url": "https://github.com/BurntSushi/aho-corasick"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/BurntSushi/aho-corasick"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.100",
+ "author": "David Tolnay ",
+ "name": "anyhow",
+ "version": "1.0.100",
+ "description": "Flexible concrete Error type built on std::error::Error",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/anyhow@1.0.100",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/anyhow"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/dtolnay/anyhow"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#arrayvec@0.7.8",
+ "author": "bluss",
+ "name": "arrayvec",
+ "version": "0.7.8",
+ "description": "A vector with fixed capacity, backed by an array (it can be stored on the stack too). Implements fixed capacity ArrayVec and ArrayString.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/arrayvec@0.7.8",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/arrayvec/"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/bluss/arrayvec"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#attribute-derive-macro@0.10.5",
+ "name": "attribute-derive-macro",
+ "version": "0.10.5",
+ "description": "Clap for proc macro attributes",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/attribute-derive-macro@0.10.5",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/attribute-derive"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ModProg/attribute-derive"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#attribute-derive@0.10.5",
+ "name": "attribute-derive",
+ "version": "0.10.5",
+ "description": "Clap like parsing for attributes in proc-macros",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/attribute-derive@0.10.5",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/attribute-derive"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ModProg/attribute-derive"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bitflags@2.10.0",
+ "author": "The Rust Project Developers",
+ "name": "bitflags",
+ "version": "2.10.0",
+ "description": "A macro to generate structures which behave like bitflags. ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/bitflags@2.10.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/bitflags"
+ },
+ {
+ "type": "website",
+ "url": "https://github.com/bitflags/bitflags"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/bitflags/bitflags"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4",
+ "author": "RustCrypto Developers",
+ "name": "block-buffer",
+ "version": "0.10.4",
+ "description": "Buffer type for block processing of data",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/block-buffer@0.10.4",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/block-buffer"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/RustCrypto/utils"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#bstr@1.12.1",
+ "author": "Andrew Gallant ",
+ "name": "bstr",
+ "version": "1.12.1",
+ "description": "A string type that is not required to be valid UTF-8.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/bstr@1.12.1",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/bstr"
+ },
+ {
+ "type": "website",
+ "url": "https://github.com/BurntSushi/bstr"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/BurntSushi/bstr"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#castaway@0.2.4",
+ "author": "Stephen M. Coakley ",
+ "name": "castaway",
+ "version": "0.2.4",
+ "description": "Safe, zero-cost downcasting for limited compile-time specialization.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/castaway@0.2.4",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/sagebind/castaway"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4",
+ "author": "Alex Crichton ",
+ "name": "cfg-if",
+ "version": "1.0.4",
+ "description": "A macro to ergonomically define an item depending on a large number of #[cfg] parameters. Structured like an if-else chain, the first matching branch is the item that gets emitted. ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/cfg-if@1.0.4",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-lang/cfg-if"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#char_str@0.0.2",
+ "author": "Charlie Marsh , ryota2357",
+ "name": "char_str",
+ "version": "0.0.2",
+ "description": "Compact owned string types forked from lean_string for Ruff and ty.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "576ba56f6ca18ebb069d0d07260407171712aff4dfe15ee3f8982dc16a455bcf"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/char_str@0.0.2",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/astral-sh/char_str"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#collection_literals@1.0.3",
+ "name": "collection_literals",
+ "version": "1.0.3",
+ "description": "Easy-to-use macros for initializing any collection",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/collection_literals@1.0.3",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/staedoix/collection_literals"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#compact_str@0.10.0",
+ "author": "Parker Timmerman ",
+ "name": "compact_str",
+ "version": "0.10.0",
+ "description": "A memory efficient string type that transparently stores strings on the stack, when possible",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/compact_str@0.10.0",
+ "externalReferences": [
+ {
+ "type": "website",
+ "url": "https://github.com/ParkMyCar/compact_str"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ParkMyCar/compact_str"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.17",
+ "author": "RustCrypto Developers",
+ "name": "cpufeatures",
+ "version": "0.2.17",
+ "description": "Lightweight runtime CPU feature detection for aarch64, loongarch64, and x86/x86_64 targets, with no_std support and support for mobile targets including Android and iOS ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/cpufeatures@0.2.17",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/cpufeatures"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/RustCrypto/utils"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.7",
+ "author": "RustCrypto Developers",
+ "name": "crypto-common",
+ "version": "0.1.7",
+ "description": "Common cryptographic traits",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/crypto-common@0.1.7",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/crypto-common"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/RustCrypto/traits"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#derive-where@1.6.0",
+ "name": "derive-where",
+ "version": "1.6.0",
+ "description": "Deriving with custom trait bounds",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/derive-where@1.6.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/derive-where"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ModProg/derive-where"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7",
+ "author": "RustCrypto Developers",
+ "name": "digest",
+ "version": "0.10.7",
+ "description": "Traits for cryptographic hash functions and message authentication codes",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/digest@0.10.7",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/digest"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/RustCrypto/traits"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#drop_bomb@0.1.5",
+ "author": "Aleksey Kladov ",
+ "name": "drop_bomb",
+ "version": "0.1.5",
+ "description": "A runtime guard for implementing linear types. ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/drop_bomb@0.1.5",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/matklad/drop_bomb"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#either@1.15.0",
+ "author": "bluss",
+ "name": "either",
+ "version": "1.15.0",
+ "description": "The enum `Either` with variants `Left` and `Right` is a general purpose sum type with two cases. ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/either@1.15.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/either/1/"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/rayon-rs/either"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.2",
+ "name": "equivalent",
+ "version": "1.0.2",
+ "description": "Traits for key comparison in maps.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "Apache-2.0 OR MIT"
+ }
+ ],
+ "purl": "pkg:cargo/equivalent@1.0.2",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/indexmap-rs/equivalent"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#filetime@0.2.29",
+ "author": "Alex Crichton ",
+ "name": "filetime",
+ "version": "0.2.29",
+ "description": "Platform-agnostic accessors of timestamps in File metadata ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/filetime@0.2.29",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/filetime"
+ },
+ {
+ "type": "website",
+ "url": "https://github.com/alexcrichton/filetime"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/alexcrichton/filetime"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7",
+ "author": "Bartłomiej Kamiński , Aaron Trent ",
+ "name": "generic-array",
+ "version": "0.14.7",
+ "description": "Generic types implementing functionality of arrays",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/generic-array@0.14.7",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "http://fizyk20.github.io/generic-array/generic_array/"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/fizyk20/generic-array.git"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#get-size-derive2@0.10.3",
+ "author": "Denis Kerp, Nicolas",
+ "name": "get-size-derive2",
+ "version": "0.10.3",
+ "description": "Derives the GetSize trait.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "c736d226c32e496b8377813b52269e11ad3a48d8373b68862d0364f04fd1229d"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/get-size-derive2@0.10.3",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/bircni/get-size2/tree/main/crates/get-size-derive2"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#get-size2@0.10.3",
+ "author": "Denis Kerp, Nicolas",
+ "name": "get-size2",
+ "version": "0.10.3",
+ "description": "Determine the size in bytes an object occupies inside RAM.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "b411f34418305908ab15a82ff78958c2a9aee9a272b2a2e663836b20a4e4b9d3"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/get-size2@0.10.3",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/bircni/get-size2"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getopts@0.2.24",
+ "author": "The Rust Project Developers",
+ "name": "getopts",
+ "version": "0.2.24",
+ "description": "getopts-like option parsing",
+ "scope": "excluded",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/getopts@0.2.24",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-lang/getopts"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.17",
+ "author": "The Rand Project Developers",
+ "name": "getrandom",
+ "version": "0.2.17",
+ "description": "A small cross-platform library for retrieving random data from system source",
+ "scope": "excluded",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/getrandom@0.2.17",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/getrandom"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-random/getrandom"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#glob@0.3.4",
+ "author": "The Rust Project Developers",
+ "name": "glob",
+ "version": "0.3.4",
+ "description": "Support for matching file paths against Unix shell style patterns. ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/glob@0.3.4",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/glob"
+ },
+ {
+ "type": "website",
+ "url": "https://github.com/rust-lang/glob"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-lang/glob"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#globset@0.4.20",
+ "author": "Andrew Gallant ",
+ "name": "globset",
+ "version": "0.4.20",
+ "description": "Cross platform single glob and glob set matching. Glob set matching is the process of matching one or more glob patterns against a single candidate path simultaneously, and returning all of the globs that matched. ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "Unlicense OR MIT"
+ }
+ ],
+ "purl": "pkg:cargo/globset@0.4.20",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/globset"
+ },
+ {
+ "type": "website",
+ "url": "https://github.com/BurntSushi/ripgrep/tree/master/crates/globset"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/BurntSushi/ripgrep/tree/master/crates/globset"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.17.1",
+ "name": "hashbrown",
+ "version": "0.17.1",
+ "description": "A Rust port of Google's SwissTable hash map",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/hashbrown@0.17.1",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-lang/hashbrown"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#heck@0.5.0",
+ "name": "heck",
+ "version": "0.5.0",
+ "description": "heck is a case conversion library.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/heck@0.5.0",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/withoutboats/heck"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#indexmap@2.14.0",
+ "name": "indexmap",
+ "version": "2.14.0",
+ "description": "A hash table with consistent order and fast iteration.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "Apache-2.0 OR MIT"
+ }
+ ],
+ "purl": "pkg:cargo/indexmap@2.14.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/indexmap/"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/indexmap-rs/indexmap"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#interpolator@0.5.0",
+ "name": "interpolator",
+ "version": "0.5.0",
+ "description": "runtime format strings, fully compatible with std's macros",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/interpolator@0.5.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/interpolator"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ModProg/interpolator"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#is-macro@0.3.7",
+ "author": "강동윤 ",
+ "name": "is-macro",
+ "version": "0.3.7",
+ "description": "Derive methods for using custom enums like Option / Result",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/is-macro@0.3.7",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/is-macro"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/dudykr/ddbase.git"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itertools@0.15.0",
+ "author": "bluss",
+ "name": "itertools",
+ "version": "0.15.0",
+ "description": "Extra iterator adaptors, iterator methods, free functions, and macros.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/itertools@0.15.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/itertools/"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-itertools/itertools"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.17",
+ "author": "David Tolnay ",
+ "name": "itoa",
+ "version": "1.0.17",
+ "description": "Fast integer primitive to string conversion",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/itoa@1.0.17",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/itoa"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/dtolnay/itoa"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#libc@0.2.189",
+ "name": "libc",
+ "version": "0.2.189",
+ "description": "Raw FFI bindings to platform libraries like libc.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/libc@0.2.189",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-lang/libc"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#log@0.4.29",
+ "author": "The Rust Project Developers",
+ "name": "log",
+ "version": "0.4.29",
+ "description": "A lightweight logging facade for Rust ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/log@0.4.29",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/log"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-lang/log"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#manyhow-macros@0.11.4",
+ "name": "manyhow-macros",
+ "version": "0.11.4",
+ "description": "Macro for manyhow",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/manyhow-macros@0.11.4",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/manyhow"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ModProg/manyhow"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#manyhow@0.11.4",
+ "name": "manyhow",
+ "version": "0.11.4",
+ "description": "proc macro error handling à la anyhow x proc-macro-error",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/manyhow@0.11.4",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/manyhow"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ModProg/manyhow"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#memchr@2.7.6",
+ "author": "Andrew Gallant , bluss",
+ "name": "memchr",
+ "version": "2.7.6",
+ "description": "Provides extremely fast (uses SIMD on x86_64, aarch64 and wasm32) routines for 1, 2 or 3 byte search and single substring search. ",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "Unlicense OR MIT"
+ }
+ ],
+ "purl": "pkg:cargo/memchr@2.7.6",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/memchr/"
+ },
+ {
+ "type": "website",
+ "url": "https://github.com/BurntSushi/memchr"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/BurntSushi/memchr"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.3",
+ "author": "Aleksey Kladov ",
+ "name": "once_cell",
+ "version": "1.21.3",
+ "description": "Single assignment cells and lazy values.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/once_cell@1.21.3",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/once_cell"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/matklad/once_cell"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ordermap@1.2.0",
+ "name": "ordermap",
+ "version": "1.2.0",
+ "description": "A hash table with consistent order and fast iteration.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "7f7476a5b122ff1fce7208e7ee9dccd0a516e835f5b8b19b8f3c98a34cf757c1"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "Apache-2.0 OR MIT"
+ }
+ ],
+ "purl": "pkg:cargo/ordermap@1.2.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/ordermap/"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/indexmap-rs/ordermap"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf@0.11.3",
+ "author": "Steven Fackler ",
+ "name": "phf",
+ "version": "0.11.3",
+ "description": "Runtime support for perfect hash function data structures",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/phf@0.11.3",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-phf/rust-phf"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.11.3",
+ "author": "Steven Fackler ",
+ "name": "phf_codegen",
+ "version": "0.11.3",
+ "description": "Codegen library for PHF types",
+ "scope": "excluded",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/phf_codegen@0.11.3",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-phf/rust-phf"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.3",
+ "author": "Steven Fackler ",
+ "name": "phf_generator",
+ "version": "0.11.3",
+ "description": "PHF generation logic",
+ "scope": "excluded",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/phf_generator@0.11.3",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-phf/rust-phf"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.3",
+ "author": "Steven Fackler ",
+ "name": "phf_shared",
+ "version": "0.11.3",
+ "description": "Support code shared by PHF libraries",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT"
+ }
+ ],
+ "purl": "pkg:cargo/phf_shared@0.11.3",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/rust-phf/rust-phf"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.21",
+ "author": "The CryptoCorrosion Contributors",
+ "name": "ppv-lite86",
+ "version": "0.2.21",
+ "description": "Cross-platform cryptography-oriented low-level SIMD library.",
+ "scope": "excluded",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/ppv-lite86@0.2.21",
+ "externalReferences": [
+ {
+ "type": "vcs",
+ "url": "https://github.com/cryptocorrosion/cryptocorrosion"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro-utils@0.10.0",
+ "name": "proc-macro-utils",
+ "version": "0.10.0",
+ "description": "low-level utilities on proc-macro and proc-macro2 types",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/proc-macro-utils@0.10.0",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/proc-macro-utils"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/ModProg/proc-macro-utils"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.106",
+ "author": "David Tolnay , Alex Crichton ",
+ "name": "proc-macro2",
+ "version": "1.0.106",
+ "description": "A substitute implementation of the compiler's `proc_macro` API to decouple token-based libraries from the procedural macro use case.",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/proc-macro2@1.0.106",
+ "externalReferences": [
+ {
+ "type": "documentation",
+ "url": "https://docs.rs/proc-macro2"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/dtolnay/proc-macro2"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-build-config@0.29.2",
+ "author": "PyO3 Project and Contributors ",
+ "name": "pyo3-build-config",
+ "version": "0.29.2",
+ "description": "Build configuration for the PyO3 ecosystem",
+ "scope": "excluded",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/pyo3-build-config@0.29.2",
+ "externalReferences": [
+ {
+ "type": "website",
+ "url": "https://github.com/pyo3/pyo3"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/pyo3/pyo3"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-ffi@0.29.2",
+ "author": "PyO3 Project and Contributors ",
+ "name": "pyo3-ffi",
+ "version": "0.29.2",
+ "description": "Python-API bindings for the PyO3 ecosystem",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/pyo3-ffi@0.29.2",
+ "externalReferences": [
+ {
+ "type": "website",
+ "url": "https://github.com/pyo3/pyo3"
+ },
+ {
+ "type": "other",
+ "url": "python"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/pyo3/pyo3"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros-backend@0.29.2",
+ "author": "PyO3 Project and Contributors ",
+ "name": "pyo3-macros-backend",
+ "version": "0.29.2",
+ "description": "Code generation for PyO3 package",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/pyo3-macros-backend@0.29.2",
+ "externalReferences": [
+ {
+ "type": "website",
+ "url": "https://github.com/pyo3/pyo3"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/pyo3/pyo3"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3-macros@0.29.2",
+ "author": "PyO3 Project and Contributors ",
+ "name": "pyo3-macros",
+ "version": "0.29.2",
+ "description": "Proc macros for PyO3 package",
+ "scope": "required",
+ "hashes": [
+ {
+ "alg": "SHA-256",
+ "content": "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
+ }
+ ],
+ "licenses": [
+ {
+ "expression": "MIT OR Apache-2.0"
+ }
+ ],
+ "purl": "pkg:cargo/pyo3-macros@0.29.2",
+ "externalReferences": [
+ {
+ "type": "website",
+ "url": "https://github.com/pyo3/pyo3"
+ },
+ {
+ "type": "vcs",
+ "url": "https://github.com/pyo3/pyo3"
+ }
+ ]
+ },
+ {
+ "type": "library",
+ "bom-ref": "registry+https://github.com/rust-lang/crates.io-index#pyo3@0.29.2",
+ "author": "PyO3 Project and Contributors