v1.3.5
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
# Copyright (C) AB Strakt
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
pyOpenSSL - A simple wrapper around the OpenSSL library
|
||||
"""
|
||||
|
||||
from OpenSSL import SSL, crypto
|
||||
from OpenSSL.version import (
|
||||
__author__,
|
||||
__copyright__,
|
||||
__email__,
|
||||
__license__,
|
||||
__summary__,
|
||||
__title__,
|
||||
__uri__,
|
||||
__version__,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"SSL",
|
||||
"__author__",
|
||||
"__copyright__",
|
||||
"__email__",
|
||||
"__license__",
|
||||
"__summary__",
|
||||
"__title__",
|
||||
"__uri__",
|
||||
"__version__",
|
||||
"crypto",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Any, Callable, NoReturn, Union
|
||||
|
||||
from cryptography.hazmat.bindings.openssl.binding import Binding
|
||||
|
||||
StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]]
|
||||
|
||||
binding = Binding()
|
||||
ffi = binding.ffi
|
||||
lib: Any = binding.lib
|
||||
|
||||
|
||||
# This is a special CFFI allocator that does not bother to zero its memory
|
||||
# after allocation. This has vastly better performance on large allocations and
|
||||
# so should be used whenever we don't need the memory zeroed out.
|
||||
no_zero_allocator = ffi.new_allocator(should_clear_after_alloc=False)
|
||||
|
||||
|
||||
def text(charp: Any) -> str:
|
||||
"""
|
||||
Get a native string type representing of the given CFFI ``char*`` object.
|
||||
|
||||
:param charp: A C-style string represented using CFFI.
|
||||
|
||||
:return: :class:`str`
|
||||
"""
|
||||
if not charp:
|
||||
return ""
|
||||
return ffi.string(charp).decode("utf-8")
|
||||
|
||||
|
||||
def exception_from_error_queue(exception_type: type[Exception]) -> NoReturn:
|
||||
"""
|
||||
Convert an OpenSSL library failure into a Python exception.
|
||||
|
||||
When a call to the native OpenSSL library fails, this is usually signalled
|
||||
by the return value, and an error code is stored in an error queue
|
||||
associated with the current thread. The err library provides functions to
|
||||
obtain these error codes and textual error messages.
|
||||
"""
|
||||
errors = []
|
||||
|
||||
while True:
|
||||
error = lib.ERR_get_error()
|
||||
if error == 0:
|
||||
break
|
||||
errors.append(
|
||||
(
|
||||
text(lib.ERR_lib_error_string(error)),
|
||||
text(lib.ERR_func_error_string(error)),
|
||||
text(lib.ERR_reason_error_string(error)),
|
||||
)
|
||||
)
|
||||
|
||||
raise exception_type(errors)
|
||||
|
||||
|
||||
def make_assert(error: type[Exception]) -> Callable[[bool], Any]:
|
||||
"""
|
||||
Create an assert function that uses :func:`exception_from_error_queue` to
|
||||
raise an exception wrapped by *error*.
|
||||
"""
|
||||
|
||||
def openssl_assert(ok: bool) -> None:
|
||||
"""
|
||||
If *ok* is not True, retrieve the error from OpenSSL and raise it.
|
||||
"""
|
||||
if ok is not True:
|
||||
exception_from_error_queue(error)
|
||||
|
||||
return openssl_assert
|
||||
|
||||
|
||||
def path_bytes(s: StrOrBytesPath) -> bytes:
|
||||
"""
|
||||
Convert a Python path to a :py:class:`bytes` for the path which can be
|
||||
passed into an OpenSSL API accepting a filename.
|
||||
|
||||
:param s: A path (valid for os.fspath).
|
||||
|
||||
:return: An instance of :py:class:`bytes`.
|
||||
"""
|
||||
b = os.fspath(s)
|
||||
|
||||
if isinstance(b, str):
|
||||
return b.encode(sys.getfilesystemencoding())
|
||||
else:
|
||||
return b
|
||||
|
||||
|
||||
def byte_string(s: str) -> bytes:
|
||||
return s.encode("charmap")
|
||||
|
||||
|
||||
# A marker object to observe whether some optional arguments are passed any
|
||||
# value or not.
|
||||
UNSPECIFIED = object()
|
||||
|
||||
_TEXT_WARNING = "str for {0} is no longer accepted, use bytes"
|
||||
|
||||
|
||||
def text_to_bytes_and_warn(label: str, obj: Any) -> Any:
|
||||
"""
|
||||
If ``obj`` is text, emit a warning that it should be bytes instead and try
|
||||
to convert it to bytes automatically.
|
||||
|
||||
:param str label: The name of the parameter from which ``obj`` was taken
|
||||
(so a developer can easily find the source of the problem and correct
|
||||
it).
|
||||
|
||||
:return: If ``obj`` is the text string type, a ``bytes`` object giving the
|
||||
UTF-8 encoding of that text is returned. Otherwise, ``obj`` itself is
|
||||
returned.
|
||||
"""
|
||||
if isinstance(obj, str):
|
||||
warnings.warn(
|
||||
_TEXT_WARNING.format(label),
|
||||
category=DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return obj.encode("utf-8")
|
||||
return obj
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
import cffi
|
||||
import cryptography
|
||||
|
||||
import OpenSSL.SSL
|
||||
|
||||
from . import version
|
||||
|
||||
_env_info = """\
|
||||
pyOpenSSL: {pyopenssl}
|
||||
cryptography: {cryptography}
|
||||
cffi: {cffi}
|
||||
cryptography's compiled against OpenSSL: {crypto_openssl_compile}
|
||||
cryptography's linked OpenSSL: {crypto_openssl_link}
|
||||
Python's OpenSSL: {python_openssl}
|
||||
Python executable: {python}
|
||||
Python version: {python_version}
|
||||
Platform: {platform}
|
||||
sys.path: {sys_path}""".format(
|
||||
pyopenssl=version.__version__,
|
||||
crypto_openssl_compile=OpenSSL._util.ffi.string(
|
||||
OpenSSL._util.lib.OPENSSL_VERSION_TEXT,
|
||||
).decode("ascii"),
|
||||
crypto_openssl_link=OpenSSL.SSL.SSLeay_version(
|
||||
OpenSSL.SSL.SSLEAY_VERSION
|
||||
).decode("ascii"),
|
||||
python_openssl=getattr(ssl, "OPENSSL_VERSION", "n/a"),
|
||||
cryptography=cryptography.__version__,
|
||||
cffi=cffi.__version__,
|
||||
python=sys.executable,
|
||||
python_version=sys.version,
|
||||
platform=sys.platform,
|
||||
sys_path=sys.path,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(_env_info)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
PRNG management routines, thin wrappers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
from OpenSSL._util import lib as _lib
|
||||
|
||||
warnings.warn(
|
||||
"OpenSSL.rand is deprecated - you should use os.urandom instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
|
||||
def add(buffer: bytes, entropy: int) -> None:
|
||||
"""
|
||||
Mix bytes from *string* into the PRNG state.
|
||||
|
||||
The *entropy* argument is (the lower bound of) an estimate of how much
|
||||
randomness is contained in *string*, measured in bytes.
|
||||
|
||||
For more information, see e.g. :rfc:`1750`.
|
||||
|
||||
This function is only relevant if you are forking Python processes and
|
||||
need to reseed the CSPRNG after fork.
|
||||
|
||||
:param buffer: Buffer with random data.
|
||||
:param entropy: The entropy (in bytes) measurement of the buffer.
|
||||
|
||||
:return: :obj:`None`
|
||||
"""
|
||||
if not isinstance(buffer, bytes):
|
||||
raise TypeError("buffer must be a byte string")
|
||||
|
||||
if not isinstance(entropy, int):
|
||||
raise TypeError("entropy must be an integer")
|
||||
|
||||
_lib.RAND_add(buffer, len(buffer), entropy)
|
||||
|
||||
|
||||
def status() -> int:
|
||||
"""
|
||||
Check whether the PRNG has been seeded with enough data.
|
||||
|
||||
:return: 1 if the PRNG is seeded enough, 0 otherwise.
|
||||
"""
|
||||
return _lib.RAND_status()
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (C) AB Strakt
|
||||
# Copyright (C) Jean-Paul Calderone
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
pyOpenSSL - A simple wrapper around the OpenSSL library
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"__author__",
|
||||
"__copyright__",
|
||||
"__email__",
|
||||
"__license__",
|
||||
"__summary__",
|
||||
"__title__",
|
||||
"__uri__",
|
||||
"__version__",
|
||||
]
|
||||
|
||||
__version__ = "26.3.0"
|
||||
|
||||
__title__ = "pyOpenSSL"
|
||||
__uri__ = "https://pyopenssl.org/"
|
||||
__summary__ = "Python wrapper module around the OpenSSL library"
|
||||
__author__ = "The pyOpenSSL developers"
|
||||
__email__ = "cryptography-dev@python.org"
|
||||
__license__ = "Apache License, Version 2.0"
|
||||
__copyright__ = f"Copyright 2001-2026 {__author__}"
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
__import__('_distutils_hack').do_override()
|
||||
@@ -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")
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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<TAB>"
|
||||
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
|
||||
@@ -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",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .terminalwriter import get_terminal_width
|
||||
from .terminalwriter import TerminalWriter
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TerminalWriter",
|
||||
"get_terminal_width",
|
||||
]
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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"<Recursion on {type(object).__name__} with id={id(object)}>"
|
||||
|
||||
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -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]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user